Advance Wayland and KDE package bring-up

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:51:06 +01:00
parent 51f3c21121
commit cf12defd28
15214 changed files with 20594243 additions and 269 deletions
@@ -0,0 +1,16 @@
add_subdirectory(core)
add_subdirectory(plugins)
if(SONNET_USE_WIDGETS)
add_subdirectory(ui)
endif()
if(SONNET_USE_QML)
add_subdirectory(quick)
endif()
ecm_qt_install_logging_categories(
EXPORT SONNET
FILE sonnet.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# Extract strings from all source files.
# EXTRACT_TR_STRINGS extracts strings with lupdate and convert them to .pot with
# lconvert.
$EXTRACT_TR_STRINGS `find . -name \*.cpp -o -name \*.h -o -name \*.ui -o -name \*.qml` -o $podir/sonnet6_qt.pot
@@ -0,0 +1,121 @@
add_library(KF6SonnetCore)
add_library(KF6::SonnetCore ALIAS KF6SonnetCore)
qt_extract_metatypes(KF6SonnetCore)
set_target_properties(KF6SonnetCore PROPERTIES
VERSION ${SONNET_VERSION}
SOVERSION ${SONNET_SOVERSION}
EXPORT_NAME SonnetCore
)
target_sources(KF6SonnetCore PRIVATE
backgroundchecker.cpp
backgroundchecker.h
backgroundchecker_p.h
client.cpp
client_p.h
guesslanguage.cpp
guesslanguage.h
languagefilter.cpp
languagefilter_p.h
loader.cpp
loader_p.h
settings.cpp
settings.h
settingsimpl.cpp
settingsimpl_p.h
speller.cpp
speller.h
spellerplugin.cpp
spellerplugin_p.h
textbreaks.cpp
textbreaks_p.h
tokenizer.cpp
tokenizer_p.h
)
# create trigrams file + add trigrams resource
if (TARGET KF6::parsetrigrams)
add_custom_command(OUTPUT "${PROJECT_BINARY_DIR}/data/trigrams.map"
COMMAND KF6::parsetrigrams "${PROJECT_SOURCE_DIR}/data/trigrams" > "${PROJECT_BINARY_DIR}/data/trigrams.map")
configure_file(${PROJECT_SOURCE_DIR}/data/trigrams.qrc.in "${PROJECT_BINARY_DIR}/data/trigrams.qrc" @ONLY)
target_sources(KF6SonnetCore PRIVATE "${PROJECT_BINARY_DIR}/data/trigrams.qrc")
endif()
ecm_qt_declare_logging_category(KF6SonnetCore
HEADER core_debug.h
IDENTIFIER SONNET_LOG_CORE
CATEGORY_NAME kf.sonnet.core
OLD_CATEGORY_NAMES sonnet.core
DESCRIPTION "Sonnet Core"
EXPORT SONNET
)
# Dear packagers, this is just used as an extra search paths for plugins. Don't get your panties in a twist.
add_definitions(-DINSTALLATION_PLUGIN_PATH="${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_PLUGINDIR}")
ecm_generate_export_header(KF6SonnetCore
BASE_NAME SonnetCore
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
VERSION_BASE_NAME Sonnet
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
)
ecm_generate_headers(SonnetCore_CamelCase_HEADERS
HEADER_NAMES
BackgroundChecker
Speller
GuessLanguage
Settings
PREFIX Sonnet
REQUIRED_HEADERS SonnetCore_HEADERS
)
target_link_libraries(KF6SonnetCore PUBLIC Qt6::Core)
target_include_directories(KF6SonnetCore
INTERFACE
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/SonnetCore>"
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/Sonnet>"
PUBLIC
"$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}>" # version header
)
install(TARGETS KF6SonnetCore EXPORT KF6SonnetTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES ${SonnetCore_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/SonnetCore/Sonnet COMPONENT Devel)
install(FILES
${SonnetCore_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/sonnetcore_export.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/SonnetCore/sonnet COMPONENT Devel)
if (BUILD_QCH)
ecm_add_qch(
KF6SonnetCore_QCH
NAME SonnetCore
BASE_NAME KF6SonnetCore
VERSION ${KF_VERSION}
ORG_DOMAIN org.kde
SOURCES # using only public headers, to cover only public API
${SonnetCore_HEADERS}
LINK_QCHS
Qt6Core_QCH
INCLUDE_DIRS
${CMAKE_CURRENT_BINARY_DIR}
BLANK_MACROS
SONNETCORE_EXPORT
SONNETCORE_DEPRECATED_EXPORT
SONNETCORE_DEPRECATED
"SONNETCORE_DEPRECATED_VERSION(x, y, t)"
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
COMPONENT Devel
)
endif()
@@ -0,0 +1,216 @@
/*
* backgroundchecker.cpp
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "backgroundchecker.h"
#include "backgroundchecker_p.h"
#include "core_debug.h"
using namespace Sonnet;
void BackgroundCheckerPrivate::start()
{
sentenceOffset = -1;
continueChecking();
}
void BackgroundCheckerPrivate::continueChecking()
{
metaObject()->invokeMethod(this, "checkNext", Qt::QueuedConnection);
}
void BackgroundCheckerPrivate::checkNext()
{
do {
// go over current sentence
while (sentenceOffset != -1 && words.hasNext()) {
Token word = words.next();
if (!words.isSpellcheckable()) {
continue;
}
// ok, this is valid word, do something
if (currentDict.isMisspelled(word.toString())) {
lastMisspelled = word;
Q_EMIT misspelling(word.toString(), word.position() + sentenceOffset);
return;
}
}
// current sentence done, grab next suitable
sentenceOffset = -1;
const bool autodetectLanguage = currentDict.testAttribute(Speller::AutoDetectLanguage);
const bool ignoreUpperCase = !currentDict.testAttribute(Speller::CheckUppercase);
while (mainTokenizer.hasNext()) {
Token sentence = mainTokenizer.next();
if (autodetectLanguage && !autoDetectLanguageDisabled) {
if (!mainTokenizer.isSpellcheckable()) {
continue;
}
// FIXME: find best from family en -> en_US, en_GB, ... ?
currentDict.setLanguage(mainTokenizer.language());
}
sentenceOffset = sentence.position();
words.setBuffer(sentence.toString());
words.setIgnoreUppercase(ignoreUpperCase);
break;
}
} while (sentenceOffset != -1);
Q_EMIT done();
}
BackgroundChecker::BackgroundChecker(QObject *parent)
: QObject(parent)
, d(new BackgroundCheckerPrivate)
{
connect(d.get(), &BackgroundCheckerPrivate::misspelling, this, &BackgroundChecker::misspelling);
connect(d.get(), &BackgroundCheckerPrivate::done, this, &BackgroundChecker::slotEngineDone);
}
BackgroundChecker::BackgroundChecker(const Speller &speller, QObject *parent)
: QObject(parent)
, d(new BackgroundCheckerPrivate)
{
d->currentDict = speller;
connect(d.get(), &BackgroundCheckerPrivate::misspelling, this, &BackgroundChecker::misspelling);
connect(d.get(), &BackgroundCheckerPrivate::done, this, &BackgroundChecker::slotEngineDone);
}
BackgroundChecker::~BackgroundChecker() = default;
void BackgroundChecker::setText(const QString &text)
{
d->mainTokenizer.setBuffer(text);
d->start();
}
void BackgroundChecker::start()
{
// ## what if d->currentText.isEmpty()?
// TODO: carry state from last buffer
d->mainTokenizer.setBuffer(fetchMoreText());
d->start();
}
void BackgroundChecker::stop()
{
// d->stop();
}
QString BackgroundChecker::fetchMoreText()
{
return QString();
}
void BackgroundChecker::finishedCurrentFeed()
{
}
bool BackgroundChecker::autoDetectLanguageDisabled() const
{
return d->autoDetectLanguageDisabled;
}
void BackgroundChecker::setAutoDetectLanguageDisabled(bool autoDetectDisabled)
{
d->autoDetectLanguageDisabled = autoDetectDisabled;
}
void BackgroundChecker::setSpeller(const Speller &speller)
{
d->currentDict = speller;
}
Speller BackgroundChecker::speller() const
{
return d->currentDict;
}
bool BackgroundChecker::checkWord(const QString &word)
{
return d->currentDict.isCorrect(word);
}
bool BackgroundChecker::addWordToPersonal(const QString &word)
{
return d->currentDict.addToPersonal(word);
}
bool BackgroundChecker::addWordToSession(const QString &word)
{
return d->currentDict.addToSession(word);
}
QStringList BackgroundChecker::suggest(const QString &word) const
{
return d->currentDict.suggest(word);
}
void BackgroundChecker::changeLanguage(const QString &lang)
{
// this sets language only for current sentence
d->currentDict.setLanguage(lang);
}
void BackgroundChecker::continueChecking()
{
d->continueChecking();
}
void BackgroundChecker::slotEngineDone()
{
finishedCurrentFeed();
const QString currentText = fetchMoreText();
if (currentText.isNull()) {
Q_EMIT done();
} else {
d->mainTokenizer.setBuffer(currentText);
d->start();
}
}
QString BackgroundChecker::text() const
{
return d->mainTokenizer.buffer();
}
QString BackgroundChecker::currentContext() const
{
int len = 60;
// we don't want the expression underneath casted to an unsigned int
// which would cause it to always evaluate to false
int currentPosition = d->lastMisspelled.position() + d->sentenceOffset;
bool begin = ((currentPosition - len / 2) <= 0) ? true : false;
QString buffer = d->mainTokenizer.buffer();
buffer.replace(currentPosition, d->lastMisspelled.length(), QStringLiteral("<b>%1</b>").arg(d->lastMisspelled.toString()));
QString context;
if (begin) {
context = QStringLiteral("%1...").arg(buffer.mid(0, len));
} else {
context = QStringLiteral("...%1...").arg(buffer.mid(currentPosition - 20, len));
}
context.replace(QLatin1Char('\n'), QLatin1Char(' '));
return context;
}
void Sonnet::BackgroundChecker::replace(int start, const QString &oldText, const QString &newText)
{
// FIXME: here we assume that replacement is in current fragment. So 'words' has
// to be adjusted and sentenceOffset does not
d->words.replace(start - (d->sentenceOffset), oldText.length(), newText);
d->mainTokenizer.replace(start, oldText.length(), newText);
}
#include "moc_backgroundchecker.cpp"
#include "moc_backgroundchecker_p.cpp"
@@ -0,0 +1,147 @@
/*
* backgroundchecker.h
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_BACKGROUNDCHECKER_H
#define SONNET_BACKGROUNDCHECKER_H
#include "speller.h"
#include "sonnetcore_export.h"
#include <QObject>
#include <memory>
/**
* The sonnet namespace.
*/
namespace Sonnet
{
class BackgroundCheckerPrivate;
class Speller;
/**
* @class Sonnet::BackgroundChecker backgroundchecker.h <Sonnet/BackgroundChecker>
*
* BackgroundChecker is used to perform spell checking without
* blocking the application. You can use it as is by calling
* the checkText function or subclass it and reimplement
* getMoreText function.
*
* The misspelling signal is emitted whenever a misspelled word
* is found. The background checker stops right before emitting
* the signal. So the parent has to call continueChecking function
* to resume the checking.
*
* done signal is emitted when whole text is spell checked.
*
* @author Zack Rusin <zack@kde.org>
* @short class used for spell checking in the background
*/
class SONNETCORE_EXPORT BackgroundChecker : public QObject
{
Q_OBJECT
public:
explicit BackgroundChecker(QObject *parent = nullptr);
explicit BackgroundChecker(const Speller &speller, QObject *parent = nullptr);
~BackgroundChecker() override;
/**
* This method is used to spell check static text.
* It automatically invokes start().
*
* Use fetchMoreText() with start() to spell check a stream.
*/
void setText(const QString &text);
QString text() const;
QString currentContext() const;
Speller speller() const;
void setSpeller(const Speller &speller);
bool checkWord(const QString &word);
QStringList suggest(const QString &word) const;
bool addWordToPersonal(const QString &word);
/**
* This method is used to add a word to the session of the
* speller currently set in BackgroundChecker.
*
* @since 5.55
*/
bool addWordToSession(const QString &word);
/**
* Returns whether the automatic language detection is disabled,
* overriding the Sonnet settings.
*
* @return true if the automatic language detection is disabled
* @since 5.71
*/
bool autoDetectLanguageDisabled() const;
/**
* Sets whether to disable the automatic language detection.
*
* @param autoDetectDisabled if true, the language will not be
* detected automatically by the spell checker, even if the option
* is enabled in the Sonnet settings.
* @since 5.71
*/
void setAutoDetectLanguageDisabled(bool autoDetectDisabled);
public Q_SLOTS:
virtual void start();
virtual void stop();
void replace(int start, const QString &oldText, const QString &newText);
void changeLanguage(const QString &lang);
/**
* After emitting misspelling signal the background
* checker stops. The catcher is responsible for calling
* continueChecking function to resume checking.
*/
virtual void continueChecking();
Q_SIGNALS:
/**
* Emitted whenever a misspelled word is found
*/
void misspelling(const QString &word, int start);
/**
* Emitted after the whole text has been spell checked.
*/
void done();
protected:
/**
* This function is called to get the text to spell check.
* It will be called continuesly until it returns QString()
* in which case the done() signal is emitted.
* Note: the start parameter in misspelling() is not a combined
* position but a position in the last string returned
* by fetchMoreText. You need to store the state in the derivatives.
*/
virtual QString fetchMoreText();
/**
* This function will be called whenever the background checker
* will be finished text which it got from fetchMoreText.
*/
virtual void finishedCurrentFeed();
protected Q_SLOTS:
void slotEngineDone();
private:
std::unique_ptr<BackgroundCheckerPrivate> const d;
};
}
#endif
@@ -0,0 +1,51 @@
/*
* backgroundchecker_p.h
*
* SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_BACKGROUNDCHECKER_P_H
#define SONNET_BACKGROUNDCHECKER_P_H
#include "backgroundchecker.h"
#include "languagefilter_p.h"
#include "speller.h"
#include "tokenizer_p.h"
#include <QObject>
namespace Sonnet
{
class BackgroundCheckerPrivate : public QObject
{
Q_OBJECT
public:
BackgroundCheckerPrivate()
: mainTokenizer(new SentenceTokenizer)
, sentenceOffset(-1)
{
autoDetectLanguageDisabled = false;
}
void start();
void continueChecking();
LanguageFilter mainTokenizer;
WordTokenizer words;
Token lastMisspelled;
Speller currentDict;
int sentenceOffset;
bool autoDetectLanguageDisabled;
private Q_SLOTS:
void checkNext();
Q_SIGNALS:
void misspelling(const QString &, int);
void done();
};
}
#endif
@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "client_p.h"
using namespace Sonnet;
Client::Client(QObject *parent)
: QObject(parent)
{
}
#include "moc_client_p.cpp"
@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_CLIENT_P_H
#define SONNET_CLIENT_P_H
#include <QObject>
#include <QString>
#include <QStringList>
#include "sonnetcore_export.h"
/*
* The fact that this class inherits from QObject makes me
* hugely unhappy. The reason for as of this writing is that
* I don't really feel like writing my own KLibFactory
* that would load anything else then QObject derivatives.
*/
namespace Sonnet
{
class SpellerPlugin;
/**
* \internal
* Client
*/
class SONNETCORE_EXPORT Client : public QObject
{
Q_OBJECT
public:
explicit Client(QObject *parent = nullptr);
/**
* @returns how reliable the answer is (higher is better).
*/
virtual int reliability() const = 0;
/**
* Returns a dictionary for the given language.
*
* @param language specifies the language of the dictionary. If an
* empty string is passed the default language will be
* used. Has to be one of the values returned by
* languages()
*
* @returns a dictionary for the language or 0 if there was an error.
*/
virtual SpellerPlugin *createSpeller(const QString &language) = 0;
/**
* @returns a list of supported languages.
*/
virtual QStringList languages() const = 0;
/**
* @returns the name of the implementing class.
*/
virtual QString name() const = 0;
};
}
Q_DECLARE_INTERFACE(Sonnet::Client, "org.kde.sonnet.Client")
#endif // SONNET_CLIENT_P_H
@@ -0,0 +1,870 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2006 Jacob R Rideout <kde@jacobrideout.net>
SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl>
SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QCoreApplication>
#include <QDataStream>
#include <QFile>
#include <QLocale>
#include <QStandardPaths>
#include "core_debug.h"
#include "guesslanguage.h"
#include "loader_p.h"
#include "speller.h"
#include "spellerplugin_p.h"
#include "tokenizer_p.h"
/*
All language tags should be valid according to IETF BCP 47, as codified in RFC 4646.
ISO 639-1 codes should be used for the language part except for cases where there
exists no code, then 639-3 codes should be used. Country codes should only be used
in special cases. Scripts can be differentiated by IANA subtags, available here:
http://www.iana.org/assignments/language-subtag-registry
The script tags correspond to ISO 15924
An overview of the best practices concerning language tagging is available here:
http://www.w3.org/International/articles/language-tags/Overview.en.php
lang tags should use underscores (_) rather than hyphens (-) to separate subsections.
EXCEPTIONS:
For cases of known differences from the above tagging scheme and major
spellcheckers such aspell/hunspell/myspell, the scheme used by the spell checkers
shall be used. All exception shall be noted here:
BCP SPELLCHECK
az-Latn az
*/
namespace Sonnet
{
class GuessLanguagePrivate
{
public:
GuessLanguagePrivate();
// language trigram score
static QHash<QString, QHash<QString, int>> s_knownModels;
void loadModels();
QList<QChar::Script> findRuns(const QString &text);
QList<QString> createOrderedModel(const QString &content);
int distance(const QList<QString> &model, const QHash<QString, int> &knownModel);
QStringList guessFromTrigrams(const QString &sample, const QStringList &langs);
QStringList identify(const QString &sample, const QList<QChar::Script> &scripts);
QString guessFromDictionaries(const QString &sentence, const QStringList &candidates);
static QSet<QString> s_knownDictionaries;
static QMultiHash<QChar::Script, QString> s_scriptLanguages;
static QMap<QString, QString> s_dictionaryNameMap;
const int MIN_LENGTH;
int m_maxItems;
double m_minConfidence;
};
QHash<QString, QHash<QString, int>> GuessLanguagePrivate::s_knownModels;
QSet<QString> GuessLanguagePrivate::s_knownDictionaries;
QMultiHash<QChar::Script, QString> GuessLanguagePrivate::s_scriptLanguages;
QMap<QString, QString> GuessLanguagePrivate::s_dictionaryNameMap;
QStringList getNames(QLocale::Script script)
{
QStringList locales;
const auto matchingLocales = QLocale::matchingLocales(QLocale::AnyLanguage, script, QLocale::AnyCountry);
locales.reserve(matchingLocales.size());
for (const QLocale &locale : matchingLocales) {
locales << locale.name();
}
return locales;
}
GuessLanguagePrivate::GuessLanguagePrivate()
: MIN_LENGTH(5)
, m_maxItems(1)
, m_minConfidence(0)
{
if (!s_scriptLanguages.isEmpty()) {
return;
}
const QStringList languages = Loader::openLoader()->languages();
s_knownDictionaries = QSet<QString>(languages.begin(), languages.end());
QSet<QString> dictionaryLanguages;
for (const QString &dictName : std::as_const(s_knownDictionaries)) {
QString languageName = QLocale(dictName).name();
if (languageName.isEmpty()) {
qCWarning(SONNET_LOG_CORE) << "Unable to parse name for dictionary" << dictName;
continue;
}
dictionaryLanguages.insert(languageName);
}
QSet<QString> allLanguages;
for (int i = 0; i < int(QChar::ScriptCount); i++) {
QChar::Script script = static_cast<QChar::Script>(i);
QStringList names;
switch (script) {
case QChar::Script_Latin:
names = getNames(QLocale::LatinScript);
break;
case QChar::Script_Greek:
names = getNames(QLocale::GreekScript);
break;
case QChar::Script_Cyrillic:
names = getNames(QLocale::CyrillicScript);
break;
case QChar::Script_Armenian:
names = getNames(QLocale::ArmenianScript);
break;
case QChar::Script_Hebrew:
names = getNames(QLocale::HebrewScript);
break;
case QChar::Script_Arabic:
names = getNames(QLocale::ArabicScript);
break;
case QChar::Script_Syriac:
names = getNames(QLocale::SyriacScript);
break;
case QChar::Script_Thaana:
names = getNames(QLocale::ThaanaScript);
break;
case QChar::Script_Devanagari:
names = getNames(QLocale::DevanagariScript);
break;
case QChar::Script_Bengali:
names = getNames(QLocale::BengaliScript);
break;
case QChar::Script_Gurmukhi:
names = getNames(QLocale::GurmukhiScript);
break;
case QChar::Script_Gujarati:
names = getNames(QLocale::GujaratiScript);
break;
case QChar::Script_Oriya:
names = getNames(QLocale::OriyaScript);
break;
case QChar::Script_Tamil:
names = getNames(QLocale::TamilScript);
break;
case QChar::Script_Telugu:
names = getNames(QLocale::TeluguScript);
break;
case QChar::Script_Kannada:
names = getNames(QLocale::KannadaScript);
break;
case QChar::Script_Malayalam:
names = getNames(QLocale::MalayalamScript);
break;
case QChar::Script_Sinhala:
names = getNames(QLocale::SinhalaScript);
break;
case QChar::Script_Thai:
names = getNames(QLocale::ThaiScript);
break;
case QChar::Script_Lao:
names = getNames(QLocale::LaoScript);
break;
case QChar::Script_Tibetan:
names = getNames(QLocale::TibetanScript);
break;
case QChar::Script_Myanmar:
names = getNames(QLocale::MyanmarScript);
break;
case QChar::Script_Georgian:
names = getNames(QLocale::GeorgianScript);
break;
case QChar::Script_Hangul:
names = getNames(QLocale::HangulScript);
break;
case QChar::Script_Ethiopic:
names = getNames(QLocale::EthiopicScript);
break;
case QChar::Script_Cherokee:
names = getNames(QLocale::CherokeeScript);
break;
case QChar::Script_CanadianAboriginal:
names = getNames(QLocale::CanadianAboriginalScript);
break;
case QChar::Script_Ogham:
names = getNames(QLocale::OghamScript);
break;
case QChar::Script_Runic:
names = getNames(QLocale::RunicScript);
break;
case QChar::Script_Khmer:
names = getNames(QLocale::KhmerScript);
break;
case QChar::Script_Mongolian:
names = getNames(QLocale::MongolianScript);
break;
case QChar::Script_Hiragana:
names = getNames(QLocale::HiraganaScript);
break;
case QChar::Script_Katakana:
names = getNames(QLocale::KatakanaScript);
break;
case QChar::Script_Bopomofo:
names = getNames(QLocale::BopomofoScript);
break;
case QChar::Script_Han:
names = getNames(QLocale::HanScript);
break;
case QChar::Script_Yi:
names = getNames(QLocale::YiScript);
break;
case QChar::Script_OldItalic:
names = getNames(QLocale::OldItalicScript);
break;
case QChar::Script_Gothic:
names = getNames(QLocale::GothicScript);
break;
case QChar::Script_Deseret:
names = getNames(QLocale::DeseretScript);
break;
case QChar::Script_Tagalog:
names = getNames(QLocale::TagalogScript);
break;
case QChar::Script_Hanunoo:
names = getNames(QLocale::HanunooScript);
break;
case QChar::Script_Buhid:
names = getNames(QLocale::BuhidScript);
break;
case QChar::Script_Tagbanwa:
names = getNames(QLocale::TagbanwaScript);
break;
case QChar::Script_Coptic:
names = getNames(QLocale::CopticScript);
break;
case QChar::Script_Limbu:
names = getNames(QLocale::LimbuScript);
break;
case QChar::Script_TaiLe:
names = getNames(QLocale::TaiLeScript);
break;
case QChar::Script_LinearB:
names = getNames(QLocale::LinearBScript);
break;
case QChar::Script_Ugaritic:
names = getNames(QLocale::UgariticScript);
break;
case QChar::Script_Shavian:
names = getNames(QLocale::ShavianScript);
break;
case QChar::Script_Osmanya:
names = getNames(QLocale::OsmanyaScript);
break;
case QChar::Script_Cypriot:
names = getNames(QLocale::CypriotScript);
break;
case QChar::Script_Braille:
names = getNames(QLocale::BrailleScript);
break;
case QChar::Script_Buginese:
names = getNames(QLocale::BugineseScript);
break;
case QChar::Script_NewTaiLue:
names = getNames(QLocale::NewTaiLueScript);
break;
case QChar::Script_Glagolitic:
names = getNames(QLocale::GlagoliticScript);
break;
case QChar::Script_Tifinagh:
names = getNames(QLocale::TifinaghScript);
break;
case QChar::Script_SylotiNagri:
names = getNames(QLocale::SylotiNagriScript);
break;
case QChar::Script_OldPersian:
names = getNames(QLocale::OldPersianScript);
break;
case QChar::Script_Kharoshthi:
names = getNames(QLocale::KharoshthiScript);
break;
case QChar::Script_Balinese:
names = getNames(QLocale::BalineseScript);
break;
case QChar::Script_Cuneiform:
names = getNames(QLocale::CuneiformScript);
break;
case QChar::Script_Phoenician:
names = getNames(QLocale::PhoenicianScript);
break;
case QChar::Script_PhagsPa:
names = getNames(QLocale::PhagsPaScript);
break;
case QChar::Script_Nko:
names = getNames(QLocale::NkoScript);
break;
case QChar::Script_Sundanese:
names = getNames(QLocale::SundaneseScript);
break;
case QChar::Script_Lepcha:
names = getNames(QLocale::LepchaScript);
break;
case QChar::Script_OlChiki:
names = getNames(QLocale::OlChikiScript);
break;
case QChar::Script_Vai:
names = getNames(QLocale::VaiScript);
break;
case QChar::Script_Saurashtra:
names = getNames(QLocale::SaurashtraScript);
break;
case QChar::Script_KayahLi:
names = getNames(QLocale::KayahLiScript);
break;
case QChar::Script_Rejang:
names = getNames(QLocale::RejangScript);
break;
case QChar::Script_Lycian:
names = getNames(QLocale::LycianScript);
break;
case QChar::Script_Carian:
names = getNames(QLocale::CarianScript);
break;
case QChar::Script_Lydian:
names = getNames(QLocale::LydianScript);
break;
case QChar::Script_Cham:
names = getNames(QLocale::ChamScript);
break;
case QChar::Script_TaiTham:
names = getNames(QLocale::LannaScript);
break;
case QChar::Script_TaiViet:
names = getNames(QLocale::TaiVietScript);
break;
case QChar::Script_Avestan:
names = getNames(QLocale::AvestanScript);
break;
case QChar::Script_EgyptianHieroglyphs:
names = getNames(QLocale::EgyptianHieroglyphsScript);
break;
case QChar::Script_Samaritan:
names = getNames(QLocale::SamaritanScript);
break;
case QChar::Script_Lisu:
names = getNames(QLocale::FraserScript);
break;
case QChar::Script_Bamum:
names = getNames(QLocale::BamumScript);
break;
case QChar::Script_Javanese:
names = getNames(QLocale::JavaneseScript);
break;
case QChar::Script_MeeteiMayek:
names = getNames(QLocale::MeiteiMayekScript);
break;
case QChar::Script_ImperialAramaic:
names = getNames(QLocale::ImperialAramaicScript);
break;
case QChar::Script_OldSouthArabian:
names = getNames(QLocale::OldSouthArabianScript);
break;
case QChar::Script_InscriptionalParthian:
names = getNames(QLocale::InscriptionalParthianScript);
break;
case QChar::Script_InscriptionalPahlavi:
names = getNames(QLocale::InscriptionalPahlaviScript);
break;
case QChar::Script_Kaithi:
names = getNames(QLocale::KaithiScript);
break;
case QChar::Script_Batak:
names = getNames(QLocale::BatakScript);
break;
case QChar::Script_Brahmi:
names = getNames(QLocale::BrahmiScript);
break;
case QChar::Script_Mandaic:
names = getNames(QLocale::MandaeanScript);
break;
case QChar::Script_Chakma:
names = getNames(QLocale::ChakmaScript);
break;
case QChar::Script_MeroiticCursive:
case QChar::Script_MeroiticHieroglyphs:
names = getNames(QLocale::MeroiticCursiveScript);
names.append(getNames(QLocale::MeroiticScript));
break;
case QChar::Script_Miao:
names = getNames(QLocale::PollardPhoneticScript);
break;
case QChar::Script_Sharada:
names = getNames(QLocale::SharadaScript);
break;
case QChar::Script_SoraSompeng:
names = getNames(QLocale::SoraSompengScript);
break;
case QChar::Script_Takri:
names = getNames(QLocale::TakriScript);
break;
case QChar::Script_CaucasianAlbanian:
names = getNames(QLocale::CaucasianAlbanianScript);
break;
case QChar::Script_BassaVah:
names = getNames(QLocale::BassaVahScript);
break;
case QChar::Script_Duployan:
names = getNames(QLocale::DuployanScript);
break;
case QChar::Script_Elbasan:
names = getNames(QLocale::ElbasanScript);
break;
case QChar::Script_Grantha:
names = getNames(QLocale::GranthaScript);
break;
case QChar::Script_PahawhHmong:
names = getNames(QLocale::PahawhHmongScript);
break;
case QChar::Script_Khojki:
names = getNames(QLocale::KhojkiScript);
break;
case QChar::Script_LinearA:
names = getNames(QLocale::LinearAScript);
break;
case QChar::Script_Mahajani:
names = getNames(QLocale::MahajaniScript);
break;
case QChar::Script_Manichaean:
names = getNames(QLocale::ManichaeanScript);
break;
case QChar::Script_MendeKikakui:
names = getNames(QLocale::MendeKikakuiScript);
break;
case QChar::Script_Modi:
names = getNames(QLocale::ModiScript);
break;
case QChar::Script_Mro:
names = getNames(QLocale::MroScript);
break;
case QChar::Script_OldNorthArabian:
names = getNames(QLocale::OldNorthArabianScript);
break;
case QChar::Script_Nabataean:
names = getNames(QLocale::NabataeanScript);
break;
case QChar::Script_Palmyrene:
names = getNames(QLocale::PalmyreneScript);
break;
case QChar::Script_PauCinHau:
names = getNames(QLocale::PauCinHauScript);
break;
case QChar::Script_OldPermic:
names = getNames(QLocale::OldPermicScript);
break;
case QChar::Script_PsalterPahlavi:
names = getNames(QLocale::PsalterPahlaviScript);
break;
case QChar::Script_Siddham:
names = getNames(QLocale::SiddhamScript);
break;
case QChar::Script_Khudawadi:
names = getNames(QLocale::KhudawadiScript);
break;
case QChar::Script_Tirhuta:
names = getNames(QLocale::TirhutaScript);
break;
case QChar::Script_WarangCiti:
names = getNames(QLocale::VarangKshitiScript);
break;
case QChar::Script_Ahom:
names = getNames(QLocale::AhomScript);
break;
case QChar::Script_AnatolianHieroglyphs:
names = getNames(QLocale::AnatolianHieroglyphsScript);
break;
case QChar::Script_Hatran:
names = getNames(QLocale::HatranScript);
break;
case QChar::Script_Multani:
names = getNames(QLocale::MultaniScript);
break;
case QChar::Script_OldHungarian:
names = getNames(QLocale::OldHungarianScript);
break;
case QChar::Script_Unknown:
case QChar::Script_Inherited:
case QChar::Script_Common:
case QChar::Script_OldTurkic:
case QChar::Script_SignWriting:
break;
default:
qCDebug(SONNET_LOG_CORE) << "Unhandled script" << script;
break;
}
allLanguages.unite(QSet<QString>(names.constBegin(), names.constEnd()));
{ // Remove unknown languages
QStringList pruned;
for (const QString &name : std::as_const(names)) {
if (!dictionaryLanguages.contains(name)) {
continue;
}
pruned.append(name);
}
names = pruned;
}
if (names.isEmpty()) {
continue;
}
for (const QString &name : std::as_const(names)) {
s_scriptLanguages.insert(script, name);
}
}
// Try to handle some badly named dictionaries
if (!allLanguages.contains(s_knownDictionaries)) {
QSet<QString> dicts(s_knownDictionaries);
dicts.subtract(allLanguages);
for (const QString &dictName : std::as_const(dicts)) {
QString languageName = QLocale(dictName).name();
if (languageName.isEmpty()) {
qCWarning(SONNET_LOG_CORE) << "Unable to parse language name" << dictName;
continue;
}
s_dictionaryNameMap[languageName] = dictName;
if (std::find(s_scriptLanguages.cbegin(), s_scriptLanguages.cend(), languageName) == s_scriptLanguages.cend()) {
qCWarning(SONNET_LOG_CORE) << "Unable to handle language from dictionary" << dictName << languageName;
}
}
}
}
GuessLanguage::GuessLanguage()
: d(new GuessLanguagePrivate)
{
}
GuessLanguage::~GuessLanguage() = default;
QString GuessLanguage::identify(const QString &text, const QStringList &suggestionsListIn) const
{
if (text.isEmpty()) {
return QString();
}
// Filter for available dictionaries
QStringList suggestionsList;
for (const QString &suggestion : suggestionsListIn) {
if (d->s_knownDictionaries.contains(suggestion) && !suggestionsList.contains(suggestion)) {
suggestionsList.append(suggestion);
}
}
// Load the model on demand
if (d->s_knownModels.isEmpty()) {
d->loadModels();
}
const QList<QChar::Script> scriptsList = d->findRuns(text);
QStringList candidateLanguages = d->identify(text, scriptsList);
// if guessing from trigrams fail
if (candidateLanguages.isEmpty()) {
for (const QChar::Script script : scriptsList) {
const auto languagesList = d->s_scriptLanguages.values(script);
for (const QString &lang : languagesList) {
if (!d->s_knownModels.contains(lang)) {
candidateLanguages.append(lang);
}
}
}
}
// Hack for some bad dictionary names
for (int i = 0; i < candidateLanguages.count(); i++) {
if (d->s_dictionaryNameMap.contains(candidateLanguages[i])) {
candidateLanguages[i] = d->s_dictionaryNameMap.value(candidateLanguages[i]);
}
}
if (candidateLanguages.count() == 1) {
return candidateLanguages.first();
}
// Wasn't able to get a good guess with the trigrams, try checking all
// dictionaries for the suggested languages.
candidateLanguages.append(suggestionsList);
candidateLanguages.removeDuplicates();
QString identified = d->guessFromDictionaries(text, candidateLanguages);
if (!identified.isEmpty()) {
return identified;
}
qCDebug(SONNET_LOG_CORE()) << "Unable to identify string with dictionaries:" << text;
// None of our methods worked, just return the best suggestion
if (!suggestionsList.isEmpty()) {
return suggestionsList.first();
}
qCDebug(SONNET_LOG_CORE) << "Unable to find any suggestion for" << text;
// Not even any suggestions, give up
return QString();
}
void GuessLanguage::setLimits(int maxItems, double minConfidence)
{
d->m_maxItems = maxItems;
d->m_minConfidence = minConfidence;
}
void GuessLanguagePrivate::loadModels()
{
// use trigrams from resource file, easy to deploy on all platforms
const QString triMapFile = QStringLiteral(":/org.kde.sonnet/trigrams.map");
qCDebug(SONNET_LOG_CORE) << "Loading trigrams from" << triMapFile;
QFile sin(triMapFile);
if (!sin.open(QIODevice::ReadOnly)) {
qCWarning(SONNET_LOG_CORE) << "Sonnet: Unable to load trigram models from file" << triMapFile;
return;
}
QDataStream in(&sin);
in >> s_knownModels;
// Sanity check
QSet<QString> availableLanguages;
QHashIterator<QString, QHash<QString, int>> iterator(s_knownModels);
while (iterator.hasNext()) {
iterator.next();
if (iterator.value().count() < MAXGRAMS) {
qCWarning(SONNET_LOG_CORE) << iterator.key() << "is has only" << iterator.value().count() << "trigrams, expected" << MAXGRAMS;
}
availableLanguages.insert(iterator.key());
}
QSet<QString> knownLanguages(s_scriptLanguages.constBegin(), s_scriptLanguages.constEnd());
knownLanguages.subtract(availableLanguages);
if (!knownLanguages.isEmpty()) {
qCDebug(SONNET_LOG_CORE) << "Missing trigrams for languages:" << knownLanguages;
}
}
QList<QChar::Script> GuessLanguagePrivate::findRuns(const QString &text)
{
QHash<QChar::Script, int> scriptCounts;
int totalCount = 0;
for (const QChar c : text) {
const QChar::Script script = c.script();
if (script == QChar::Script_Common || script == QChar::Script_Inherited) {
continue;
}
if (!c.isLetter()) {
continue;
}
scriptCounts[script]++;
totalCount++;
}
QList<QChar::Script> relevantScripts;
if (totalCount == 0) {
return relevantScripts;
}
if (scriptCounts.size() == 1) {
return {scriptCounts.cbegin().key()};
}
for (auto it = scriptCounts.cbegin(); it != scriptCounts.cend(); ++it) {
// return run types that used for 40% or more of the string
const int scriptCount = it.value();
const auto currentScript = it.key();
if (scriptCount * 100 / totalCount >= 40) {
relevantScripts << currentScript;
// always return basic latin if found more than 15%.
} else if (currentScript == QChar::Script_Latin && scriptCount * 100 / totalCount >= 15) {
relevantScripts << currentScript;
}
}
return relevantScripts;
}
QStringList GuessLanguagePrivate::identify(const QString &sample, const QList<QChar::Script> &scripts)
{
if (sample.size() < MIN_LENGTH) {
return QStringList();
}
QStringList guesses;
for (const QChar::Script script : scripts) {
guesses.append(guessFromTrigrams(sample, s_scriptLanguages.values(script)));
}
return guesses;
}
QStringList GuessLanguagePrivate::guessFromTrigrams(const QString &sample, const QStringList &languages)
{
QStringList ret;
const QList<QString> sampleTrigrams = createOrderedModel(sample);
// Sort by score
QMultiMap<int, QString> scores;
for (const QString &language : languages) {
if (s_knownModels.contains(language)) {
scores.insert(distance(sampleTrigrams, s_knownModels[language]), language);
}
}
// Skip if either no results or best result is completely unknown (distance >= maxdistance)
if (scores.isEmpty() || scores.firstKey() >= MAXGRAMS * sampleTrigrams.size()) {
qCDebug(SONNET_LOG_CORE) << "No scores for" << sample;
return ret;
}
int counter = 0;
double confidence = 0;
QMultiMapIterator<int, QString> it(scores);
it.next();
QString prevItem = it.value();
int prevScore = it.key();
while (it.hasNext() && counter < m_maxItems && confidence < m_minConfidence) {
it.next();
counter++;
confidence += (it.key() - prevScore) / (double)it.key();
ret += prevItem;
prevItem = it.value();
prevScore = it.key();
}
if (counter < m_maxItems && confidence < m_minConfidence) {
ret += prevItem;
}
return ret;
}
QList<QString> GuessLanguagePrivate::createOrderedModel(const QString &content)
{
QHash<QString, int> trigramCounts;
// collect trigrams
trigramCounts.reserve(content.size() - 2);
for (int i = 0; i < (content.size() - 2); ++i) {
QString tri = content.mid(i, 3).toLower();
trigramCounts[tri]++;
}
// invert the map <freq, trigram>
QList<QPair<int, QString>> trigramFrequencyList;
trigramFrequencyList.reserve(trigramCounts.size());
auto it = trigramCounts.constBegin();
for (; it != trigramCounts.constEnd(); ++it) {
const QChar *data = it.key().constData();
bool hasTwoSpaces = (data[1].isSpace() && (data[0].isSpace() || data[2].isSpace()));
if (!hasTwoSpaces) {
const int freq = it.value();
const QString &trigram = it.key();
trigramFrequencyList.append({freq, trigram});
}
}
// sort descending by frequency
std::sort(trigramFrequencyList.begin(), trigramFrequencyList.end(), [](const QPair<int, QString> &a, const QPair<int, QString> &b) {
return a.first > b.first;
});
QList<QString> orderedTrigrams;
orderedTrigrams.reserve(trigramFrequencyList.size());
for (const auto &tri : std::as_const(trigramFrequencyList)) {
orderedTrigrams.append(tri.second);
}
return orderedTrigrams;
}
int GuessLanguagePrivate::distance(const QList<QString> &model, const QHash<QString, int> &knownModel)
{
int counter = -1;
int dist = 0;
for (const QString &trigram : model) {
const int val = knownModel.value(trigram, -1);
if (val != -1) {
dist += qAbs(++counter - val);
} else {
dist += MAXGRAMS;
}
if (counter == (MAXGRAMS - 1)) {
break;
}
}
return dist;
}
QString GuessLanguagePrivate::guessFromDictionaries(const QString &sentence, const QStringList &candidates)
{
// Try to see how many languages we can get spell checking for
QList<QSharedPointer<SpellerPlugin>> spellers;
for (const QString &lang : candidates) {
if (!Loader::openLoader()->languages().contains(lang)) {
qCWarning(SONNET_LOG_CORE) << "Dictionary asked for invalid speller" << lang;
continue;
}
QSharedPointer<SpellerPlugin> plugin = Loader::openLoader()->cachedSpeller(lang);
if (!plugin.isNull()) {
spellers.append(plugin);
}
}
// If there's no spell checkers, give up
if (spellers.isEmpty()) {
return QString();
}
QMap<QString, int> correctHits;
WordTokenizer tokenizer(sentence);
while (tokenizer.hasNext()) {
Token word = tokenizer.next();
if (!tokenizer.isSpellcheckable()) {
continue;
}
for (int i = 0; i < spellers.count(); ++i) {
if (spellers[i]->isCorrect(word.toString())) {
correctHits[spellers[i]->language()]++;
}
}
}
if (correctHits.isEmpty()) {
return QString();
}
QMap<QString, int>::const_iterator max = correctHits.constBegin();
for (QMap<QString, int>::const_iterator itr = correctHits.constBegin(); itr != correctHits.constEnd(); ++itr) {
if (itr.value() > max.value()) {
max = itr;
}
}
return max.key();
}
}
@@ -0,0 +1,84 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2006 Jacob R Rideout <kde@jacobrideout.net>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef GUESSLANGUAGE_H
#define GUESSLANGUAGE_H
#include <QString>
#include <QStringList>
#include "sonnetcore_export.h"
#include <memory>
namespace Sonnet
{
// Amount of trigrams in each file
static const int MAXGRAMS = 300;
class GuessLanguagePrivate;
/**
* @class Sonnet::GuessLanguage guesslanguage.h <Sonnet/GuessLanguage>
*
* @short GuessLanguage determines the language of a given text.
*
* GuessLanguage can determine the difference between ~75 languages for a given string. It is
* based off a Perl script originally written by Maciej Ceglowski <maciej@ceglowski.com>
* called Languid. His script used a 2 part heuristic to determine language. First the text
* is checked for the scripts it contains, then for each set of languages using those
* scripts a n-gram frequency model of a given language is compared to a model of the text.
* The most similar language model is assumed to be the language. If no language is found
* an empty string is returned.
*
*
* @author Jacob Rideout <kde@jacobrideout.net>
* @since 4.3
*/
class SONNETCORE_EXPORT GuessLanguage
{
public:
/** Constructor
* Creates a new GuessLanguage instance. If @p text is specified,
* it sets the text to be checked.
* @param text the text that is to be checked
*/
GuessLanguage();
/** Destructor
*/
~GuessLanguage();
GuessLanguage(const GuessLanguage &) = delete;
GuessLanguage &operator=(const GuessLanguage &) = delete;
/**
* Sets limits to number of languages returned by identify(). The confidence for each language is computed
* as difference between this and next language on the list normalized to 0-1 range. Reasonable value to get
* fairly sure result is 0.1 . Default is returning best guess without caring about confidence - exactly
* as after call to setLimits(1,0).
* @param maxItems The list returned by identify() will never have more than maxItems item
* @param minConfidence The list will have only enough items for their summary confidence equal
* or exceed minConfidence.
*/
void setLimits(int maxItems, double minConfidence);
/**
* Returns the 2 digit ISO 639-1 code for the language of the currently
* set text and. Three digits are returned only in the case where a 2 digit
* code does not exist. If @p text isn't empty, set the text to checked.
* @param text to be identified
* @return list of the presumed languages of the text, sorted by decreasing confidence. Empty list means
* it is impossible to determine language with confidence required by setLimits
*/
QString identify(const QString &text, const QStringList &suggestions = QStringList()) const;
private:
std::unique_ptr<GuessLanguagePrivate> const d;
};
}
#endif
@@ -0,0 +1,140 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QString>
#include "guesslanguage.h"
#include "languagefilter_p.h"
#include "loader_p.h"
#include "settingsimpl_p.h"
#include "speller.h"
namespace Sonnet
{
#define MIN_RELIABILITY 0.1
#define MAX_ITEMS 5
class LanguageFilterPrivate
{
public:
LanguageFilterPrivate(AbstractTokenizer *s)
: source(s)
{
gl.setLimits(MAX_ITEMS, MIN_RELIABILITY);
}
~LanguageFilterPrivate()
{
delete source;
}
QString mainLanguage() const;
AbstractTokenizer *source = nullptr;
Token lastToken;
mutable QString lastLanguage;
mutable QString cachedMainLanguage;
QString prevLanguage;
GuessLanguage gl;
Speller sp;
};
QString LanguageFilterPrivate::mainLanguage() const
{
if (cachedMainLanguage.isNull()) {
cachedMainLanguage = gl.identify(source->buffer(), QStringList(Loader::openLoader()->settings()->defaultLanguage()));
}
return cachedMainLanguage;
}
/* -----------------------------------------------------------------*/
LanguageFilter::LanguageFilter(AbstractTokenizer *source)
: d(new LanguageFilterPrivate(source))
{
d->prevLanguage = Loader::openLoader()->settings()->defaultLanguage();
}
LanguageFilter::LanguageFilter(const LanguageFilter &other)
: d(new LanguageFilterPrivate(other.d->source))
{
d->lastToken = other.d->lastToken;
d->lastLanguage = other.d->lastLanguage;
d->cachedMainLanguage = other.d->cachedMainLanguage;
d->prevLanguage = other.d->prevLanguage;
}
LanguageFilter::~LanguageFilter() = default;
bool LanguageFilter::hasNext() const
{
return d->source->hasNext();
}
void LanguageFilter::setBuffer(const QString &buffer)
{
d->cachedMainLanguage = QString();
d->source->setBuffer(buffer);
}
Token LanguageFilter::next()
{
d->lastToken = d->source->next();
d->prevLanguage = d->lastLanguage;
d->lastLanguage = QString();
return d->lastToken;
}
QString LanguageFilter::language() const
{
if (d->lastLanguage.isNull()) {
d->lastLanguage = d->gl.identify(d->lastToken.toString(), QStringList() << d->prevLanguage << Loader::openLoader()->settings()->defaultLanguage());
}
const QStringList available = d->sp.availableLanguages();
// FIXME: do something a little more smart here
if (!available.contains(d->lastLanguage)) {
for (const QString &lang : available) {
if (lang.startsWith(d->lastLanguage)) {
d->lastLanguage = lang;
break;
}
}
}
return d->lastLanguage;
}
bool LanguageFilter::isSpellcheckable() const
{
const QString &lastlang = language();
if (lastlang.isEmpty()) {
return false;
}
if (d->sp.availableLanguages().contains(lastlang)) {
return true;
}
return false;
}
QString LanguageFilter::buffer() const
{
return d->source->buffer();
}
void LanguageFilter::replace(int position, int len, const QString &newWord)
{
d->source->replace(position, len, newWord);
// FIXME: fix lastToken
// uncache language for current token - it may have changed
d->lastLanguage = QString();
}
}
@@ -0,0 +1,58 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef LANGUAGEFILTER_H
#define LANGUAGEFILTER_H
#include "sonnetcore_export.h"
#include <tokenizer_p.h>
#include <QString>
#include <memory>
namespace Sonnet
{
class LanguageFilterPrivate;
/**
@short Deternmines language for fragments of text
This class takes fragments produced by supplied tokenizer and provides additional information:
language used in each fragment and if there is spell and grammar checker suitable for the fragment.
*/
class SONNETCORE_EXPORT LanguageFilter : public AbstractTokenizer
{
public:
/** Creates language filter for given tokenizer. LanguageFilter takes complete ownership of given tokenizer.
This means that no source's methods should be called anymore.
*/
LanguageFilter(AbstractTokenizer *source);
LanguageFilter(const LanguageFilter &other);
~LanguageFilter() override;
/** Language for token last returned by next() */
QString language() const;
/** Returns true if there is spellchecker installed for last token's language */
bool isSpellcheckable() const;
/** Returns true if there is grammar checker installed for last token's language */
// bool isGrammarCheckable() const;
void setBuffer(const QString &buffer) override;
bool hasNext() const override;
Token next() override;
QString buffer() const override;
void replace(int position, int len, const QString &newWord) override;
private:
std::unique_ptr<LanguageFilterPrivate> const d;
};
}
#endif
@@ -0,0 +1,354 @@
/*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2012 Martin Sandsmark <martin.sandsmark@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "client_p.h"
#include "loader_p.h"
#include "settingsimpl_p.h"
#include "spellerplugin_p.h"
#include "core_debug.h"
#include <QCoreApplication>
#include <QDir>
#include <QHash>
#include <QList>
#include <QLocale>
#include <QMap>
#include <QPluginLoader>
#include <algorithm>
#ifdef SONNET_STATIC
#include "../plugins/hunspell/hunspellclient.h"
#ifdef Q_OS_MACOS
#include "../plugins/nsspellchecker/nsspellcheckerclient.h"
#endif
#endif
namespace Sonnet
{
class LoaderPrivate
{
public:
SettingsImpl *settings;
// <language, Clients with that language >
QMap<QString, QList<Client *>> languageClients;
QStringList clients;
QSet<QString> loadedPlugins;
QStringList languagesNameCache;
QHash<QString, QSharedPointer<SpellerPlugin>> spellerCache;
};
Q_GLOBAL_STATIC(Loader, s_loader)
Loader *Loader::openLoader()
{
if (s_loader.isDestroyed()) {
return nullptr;
}
return s_loader();
}
Loader::Loader()
: d(new LoaderPrivate)
{
d->settings = new SettingsImpl(this);
d->settings->restore();
loadPlugins();
}
Loader::~Loader()
{
qCDebug(SONNET_LOG_CORE) << "Removing loader: " << this;
delete d->settings;
d->settings = nullptr;
}
SpellerPlugin *Loader::createSpeller(const QString &language, const QString &clientName) const
{
QString backend = clientName;
QString plang = language;
if (plang.isEmpty()) {
plang = d->settings->defaultLanguage();
}
auto clientsItr = d->languageClients.constFind(plang);
if (clientsItr == d->languageClients.constEnd()) {
if (language.isEmpty() || language == QStringLiteral("C")) {
qCDebug(SONNET_LOG_CORE) << "No language dictionaries for the language:" << plang << "trying to load en_US as default";
return createSpeller(QStringLiteral("en_US"), clientName);
}
qCDebug(SONNET_LOG_CORE) << "No language dictionaries for the language:" << plang;
Q_EMIT loadingDictionaryFailed(plang);
return nullptr;
}
const QList<Client *> lClients = *clientsItr;
if (backend.isEmpty()) {
backend = d->settings->defaultClient();
if (!backend.isEmpty()) {
// check if the default client supports the requested language;
// if it does it will be an element of lClients.
bool unknown = !std::any_of(lClients.constBegin(), lClients.constEnd(), [backend](const Client *client) {
return client->name() == backend;
});
if (unknown) {
qCWarning(SONNET_LOG_CORE) << "Default client" << backend << "doesn't support language:" << plang;
backend = QString();
}
}
}
QListIterator<Client *> itr(lClients);
while (itr.hasNext()) {
Client *item = itr.next();
if (!backend.isEmpty()) {
if (backend == item->name()) {
SpellerPlugin *dict = item->createSpeller(plang);
qCDebug(SONNET_LOG_CORE) << "Using the" << item->name() << "plugin for language" << plang;
return dict;
}
} else {
// the first one is the one with the highest
// reliability
SpellerPlugin *dict = item->createSpeller(plang);
qCDebug(SONNET_LOG_CORE) << "Using the" << item->name() << "plugin for language" << plang;
return dict;
}
}
qCWarning(SONNET_LOG_CORE) << "The default client" << backend << "has no language dictionaries for the language:" << plang;
return nullptr;
}
QSharedPointer<SpellerPlugin> Loader::cachedSpeller(const QString &language)
{
auto &speller = d->spellerCache[language];
if (!speller) {
speller.reset(createSpeller(language));
}
return speller;
}
void Loader::clearSpellerCache()
{
d->spellerCache.clear();
}
QStringList Loader::clients() const
{
return d->clients;
}
QStringList Loader::languages() const
{
return d->languageClients.keys();
}
QString Loader::languageNameForCode(const QString &langCode) const
{
QString currentDictionary = langCode; // e.g. en_GB-ize-wo_accents
QString isoCode; // locale ISO name
QString variantName; // dictionary variant name e.g. w_accents
QString localizedLang; // localized language
QString localizedCountry; // localized country
QString localizedVariant;
QByteArray variantEnglish; // dictionary variant in English
int minusPos; // position of "-" char
int variantCount = 0; // used to iterate over variantList
struct variantListType {
const char *variantShortName;
const char *variantEnglishName;
};
/*
* This redefines the QT_TRANSLATE_NOOP3 macro provided by Qt to indicate that
* statically initialised text should be translated so that it expands to just
* the string that should be translated, making it possible to use it in the
* single string construct below.
*/
#undef QT_TRANSLATE_NOOP3
#define QT_TRANSLATE_NOOP3(a, b, c) b
const variantListType variantList[] = {{"40", QT_TRANSLATE_NOOP3("Sonnet::Loader", "40", "dictionary variant")}, // what does 40 mean?
{"60", QT_TRANSLATE_NOOP3("Sonnet::Loader", "60", "dictionary variant")}, // what does 60 mean?
{"80", QT_TRANSLATE_NOOP3("Sonnet::Loader", "80", "dictionary variant")}, // what does 80 mean?
{"ise", QT_TRANSLATE_NOOP3("Sonnet::Loader", "-ise suffixes", "dictionary variant")},
{"ize", QT_TRANSLATE_NOOP3("Sonnet::Loader", "-ize suffixes", "dictionary variant")},
{"ise-w_accents", QT_TRANSLATE_NOOP3("Sonnet::Loader", "-ise suffixes and with accents", "dictionary variant")},
{"ise-wo_accents", QT_TRANSLATE_NOOP3("Sonnet::Loader", "-ise suffixes and without accents", "dictionary variant")},
{"ize-w_accents", QT_TRANSLATE_NOOP3("Sonnet::Loader", "-ize suffixes and with accents", "dictionary variant")},
{"ize-wo_accents", QT_TRANSLATE_NOOP3("Sonnet::Loader", "-ize suffixes and without accents", "dictionary variant")},
{"lrg", QT_TRANSLATE_NOOP3("Sonnet::Loader", "large", "dictionary variant")},
{"med", QT_TRANSLATE_NOOP3("Sonnet::Loader", "medium", "dictionary variant")},
{"sml", QT_TRANSLATE_NOOP3("Sonnet::Loader", "small", "dictionary variant")},
{"variant_0", QT_TRANSLATE_NOOP3("Sonnet::Loader", "variant 0", "dictionary variant")},
{"variant_1", QT_TRANSLATE_NOOP3("Sonnet::Loader", "variant 1", "dictionary variant")},
{"variant_2", QT_TRANSLATE_NOOP3("Sonnet::Loader", "variant 2", "dictionary variant")},
{"wo_accents", QT_TRANSLATE_NOOP3("Sonnet::Loader", "without accents", "dictionary variant")},
{"w_accents", QT_TRANSLATE_NOOP3("Sonnet::Loader", "with accents", "dictionary variant")},
{"ye", QT_TRANSLATE_NOOP3("Sonnet::Loader", "with ye, modern russian", "dictionary variant")},
{"yeyo", QT_TRANSLATE_NOOP3("Sonnet::Loader", "with yeyo, modern and old russian", "dictionary variant")},
{"yo", QT_TRANSLATE_NOOP3("Sonnet::Loader", "with yo, old russian", "dictionary variant")},
{"extended", QT_TRANSLATE_NOOP3("Sonnet::Loader", "extended", "dictionary variant")},
{nullptr, nullptr}};
minusPos = currentDictionary.indexOf(QLatin1Char('-'));
if (minusPos != -1) {
variantName = currentDictionary.right(currentDictionary.length() - minusPos - 1);
while (variantList[variantCount].variantShortName != nullptr) {
if (QLatin1String(variantList[variantCount].variantShortName) == variantName) {
break;
} else {
variantCount++;
}
}
if (variantList[variantCount].variantShortName != nullptr) {
variantEnglish = variantList[variantCount].variantEnglishName;
} else {
variantEnglish = variantName.toLatin1();
}
localizedVariant = tr(variantEnglish.constData(), "dictionary variant");
isoCode = currentDictionary.left(minusPos);
} else {
isoCode = currentDictionary;
}
QLocale locale(isoCode);
localizedCountry = locale.nativeTerritoryName();
localizedLang = locale.nativeLanguageName();
if (localizedLang.isEmpty() && localizedCountry.isEmpty()) {
return isoCode; // We have nothing
}
if (!localizedCountry.isEmpty() && !localizedVariant.isEmpty()) { // We have both a country name and a variant
return tr("%1 (%2) [%3]", "dictionary name; %1 = language name, %2 = country name and %3 = language variant name")
.arg(localizedLang, localizedCountry, localizedVariant);
} else if (!localizedCountry.isEmpty()) { // We have a country name
return tr("%1 (%2)", "dictionary name; %1 = language name, %2 = country name").arg(localizedLang, localizedCountry);
} else { // We only have a language name
return localizedLang;
}
}
QStringList Loader::languageNames() const
{
/* For whatever reason languages() might change. So,
* to be in sync with it let's do the following check.
*/
if (d->languagesNameCache.count() == languages().count()) {
return d->languagesNameCache;
}
QStringList allLocalizedDictionaries;
for (const QString &langCode : languages()) {
allLocalizedDictionaries.append(languageNameForCode(langCode));
}
// cache the list
d->languagesNameCache = allLocalizedDictionaries;
return allLocalizedDictionaries;
}
SettingsImpl *Loader::settings() const
{
return d->settings;
}
void Loader::loadPlugins()
{
#ifndef SONNET_STATIC
const QStringList libPaths = QCoreApplication::libraryPaths() << QStringLiteral(INSTALLATION_PLUGIN_PATH);
const QString pathSuffix(QStringLiteral("/kf6/sonnet/"));
for (const QString &libPath : libPaths) {
QDir dir(libPath + pathSuffix);
if (!dir.exists()) {
continue;
}
for (const QString &fileName : dir.entryList(QDir::Files)) {
loadPlugin(dir.absoluteFilePath(fileName));
}
}
if (d->loadedPlugins.isEmpty()) {
qCWarning(SONNET_LOG_CORE) << "Sonnet: No speller backends available!";
}
#else
#ifdef Q_OS_MACOS
loadPlugin(QString());
#endif
loadPlugin(QStringLiteral("Hunspell"));
#endif
}
void Loader::loadPlugin(const QString &pluginPath)
{
#ifndef SONNET_STATIC
QPluginLoader plugin(pluginPath);
const QString pluginIID = plugin.metaData()[QStringLiteral("IID")].toString();
if (!pluginIID.isEmpty()) {
if (d->loadedPlugins.contains(pluginIID)) {
qCDebug(SONNET_LOG_CORE) << "Skipping already loaded" << pluginPath;
return;
}
d->loadedPlugins.insert(pluginIID);
}
if (!plugin.load()) { // We do this separately for better error handling
qCDebug(SONNET_LOG_CORE) << "Sonnet: Unable to load plugin" << pluginPath << "Error:" << plugin.errorString();
d->loadedPlugins.remove(pluginIID);
return;
}
Client *client = qobject_cast<Client *>(plugin.instance());
if (!client) {
qCWarning(SONNET_LOG_CORE) << "Sonnet: Invalid plugin loaded" << pluginPath;
plugin.unload(); // don't leave it in memory
return;
}
#else
Client *client = nullptr;
if (pluginPath == QLatin1String("Hunspell")) {
client = new HunspellClient(this);
}
#ifdef Q_OS_MACOS
else {
client = new NSSpellCheckerClient(this);
}
#endif
#endif
const QStringList languages = client->languages();
d->clients.append(client->name());
for (const QString &language : languages) {
QList<Client *> &languageClients = d->languageClients[language];
if (languageClients.isEmpty() //
|| client->reliability() < languageClients.first()->reliability()) {
languageClients.append(client); // less reliable, to the end
} else {
languageClients.prepend(client); // more reliable, to the front
}
}
}
void Loader::changed()
{
Q_EMIT configurationChanged();
}
}
#include "moc_loader_p.cpp"
@@ -0,0 +1,140 @@
/*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_LOADER_P_H
#define SONNET_LOADER_P_H
#include "sonnetcore_export.h"
#include <QObject>
#include <QSharedPointer>
#include <QString>
#include <QStringList>
#include <memory>
namespace Sonnet
{
class SettingsImpl;
class SpellerPlugin;
class LoaderPrivate;
/**
* \internal
* @short Class used to deal with dictionaries
*
* This class manages all dictionaries. It's the top level
* Sonnet class, you can think of it as the kernel or manager
* of the Sonnet architecture.
*/
class SONNETCORE_EXPORT Loader : public QObject
{
Q_OBJECT
public:
/**
* Constructs the loader.
*
* It's very important that you leave the return value in a Loader::Ptr.
* Loader is reference counted so if you don't want to have it deleted
* under you simply have to hold it in a Loader::Ptr for as long as
* you're using it.
*/
static Loader *openLoader();
public:
Loader();
~Loader() override;
/**
* Returns dictionary for the given language and preferred client.
*
* @param language specifies the language of the dictionary. If an
* empty string will be passed the default language will
* be used. Has to be one of the values returned by
* \ref languages()
* @param client specifies the preferred client. If no client is
* specified a client which supports the given
* language is picked. If a few clients supports
* the same language the one with the biggest
* reliability value is returned.
*
*/
SpellerPlugin *createSpeller(const QString &language = QString(), const QString &client = QString()) const;
/**
* Returns a shared, cached, dictionary for the given language.
*
* @param language specifies the language of the dictionary. If an
* empty string will be passed the default language will
* be used. Has to be one of the values returned by
* \ref languages()
*/
QSharedPointer<SpellerPlugin> cachedSpeller(const QString &language);
/**
* Returns a shared, cached, dictionary for the given language.
*
* @param language specifies the language of the dictionary. If an
* empty string will be passed the default language will
* be used. Has to be one of the values returned by
* \ref languages()
*/
void clearSpellerCache();
/**
* Returns names of all supported clients (e.g. ISpell, ASpell)
*/
QStringList clients() const;
/**
* Returns a list of supported languages.
*/
QStringList languages() const;
/**
* Returns a localized list of names of supported languages.
*/
QStringList languageNames() const;
/**
* @param langCode the dictionary name/language code, e.g. en_gb
* @return the localized language name, e.g. "British English"
* @since 4.2
*/
QString languageNameForCode(const QString &langCode) const;
/**
* Returns the SettingsImpl object used by the loader.
*/
SettingsImpl *settings() const;
Q_SIGNALS:
/**
* Signal is emitted whenever the SettingsImpl object
* associated with this Loader changes.
*/
void configurationChanged();
/**
* Emitted when loading a dictionary fails, so that Ui parts can
* display an appropriate error message informing the user about
* the issue.
* @param language the name of the dictionary that failed to be loaded
* @since 5.56
*/
void loadingDictionaryFailed(const QString &language) const;
protected:
friend class SettingsImpl;
void changed();
private:
SONNETCORE_NO_EXPORT void loadPlugins();
SONNETCORE_NO_EXPORT void loadPlugin(const QString &pluginPath);
private:
std::unique_ptr<LoaderPrivate> const d;
};
}
#endif // SONNET_LOADER_P_H
@@ -0,0 +1,349 @@
/*
* SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "settingsimpl_p.h"
#include <QLocale>
#include "loader_p.h"
#include "settings.h"
#include <QDebug>
#include <speller.h>
namespace Sonnet
{
class DictionaryModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit DictionaryModel(QObject *parent = nullptr)
: QAbstractListModel(parent)
{
reload();
}
~DictionaryModel() override
{
}
void reload()
{
beginResetModel();
Sonnet::Speller speller;
m_preferredDictionaries = speller.preferredDictionaries();
m_availableDictionaries = speller.availableDictionaries();
endResetModel();
}
void setDefaultLanguage(const QString &language)
{
m_defaultDictionary = language;
Q_EMIT dataChanged(index(0, 0), index(rowCount(QModelIndex()) - 1, 0), {Settings::DefaultRole});
}
bool setData(const QModelIndex &idx, const QVariant &value, int role = Qt::EditRole) override
{
Q_UNUSED(value)
if (!checkIndex(idx) || role != Qt::CheckStateRole) {
return false;
}
const int row = idx.row();
const auto language = m_availableDictionaries.keys().at(row);
const auto inPreferredDictionaries = m_preferredDictionaries.contains(m_availableDictionaries.keys().at(row));
if (inPreferredDictionaries) {
m_preferredDictionaries.remove(language);
} else {
m_preferredDictionaries[language] = m_availableDictionaries.values().at(row);
}
qobject_cast<Settings *>(parent())->setPreferredLanguages(m_preferredDictionaries.values());
Q_EMIT dataChanged(index(row, 0), index(row, 0), {Qt::CheckStateRole});
return true;
}
QVariant data(const QModelIndex &index, int role) const override
{
if (!checkIndex(index)) {
return {};
}
const int row = index.row();
switch (role) {
case Qt::DisplayRole:
return m_availableDictionaries.keys().at(row);
case Settings::LanguageCodeRole:
return m_availableDictionaries.values().at(row);
case Qt::CheckStateRole:
return m_preferredDictionaries.contains(m_availableDictionaries.keys().at(row));
case Settings::DefaultRole:
return data(index, Settings::LanguageCodeRole) == m_defaultDictionary;
}
return {};
}
int rowCount(const QModelIndex &parent) const override
{
Q_UNUSED(parent)
return m_availableDictionaries.count();
}
QHash<int, QByteArray> roleNames() const override
{
return {
{Qt::DisplayRole, QByteArrayLiteral("display")},
{Qt::CheckStateRole, QByteArrayLiteral("checked")},
{Settings::PreferredRole, QByteArrayLiteral("isPreferred")},
{Settings::LanguageCodeRole, QByteArrayLiteral("languageCode")},
{Settings::DefaultRole, QByteArrayLiteral("isDefault")},
};
}
private:
QMap<QString, QString> m_preferredDictionaries;
QMap<QString, QString> m_availableDictionaries;
QString m_defaultDictionary;
};
class SettingsPrivate
{
public:
Loader *loader = nullptr;
DictionaryModel *dictionaryModel = nullptr;
};
Settings::Settings(QObject *parent)
: QObject(parent)
, d(new SettingsPrivate)
{
d->loader = Loader::openLoader();
}
Settings::~Settings() = default;
void Settings::setDefaultLanguage(const QString &lang)
{
if (defaultLanguage() == lang) {
return;
}
d->loader->settings()->setDefaultLanguage(lang);
Q_EMIT defaultLanguageChanged();
Q_EMIT modifiedChanged();
if (d->dictionaryModel) {
d->dictionaryModel->setDefaultLanguage(lang);
}
}
QString Settings::defaultLanguage() const
{
return d->loader->settings()->defaultLanguage();
}
void Settings::setPreferredLanguages(const QStringList &lang)
{
if (!d->loader->settings()->setPreferredLanguages(lang)) {
return;
}
Q_EMIT modifiedChanged();
Q_EMIT preferredLanguagesChanged();
}
QStringList Settings::preferredLanguages() const
{
return d->loader->settings()->preferredLanguages();
}
void Settings::setDefaultClient(const QString &client)
{
if (!d->loader->settings()->setDefaultClient(client)) {
return;
}
Q_EMIT defaultClientChanged();
Q_EMIT modifiedChanged();
}
QString Settings::defaultClient() const
{
return d->loader->settings()->defaultClient();
}
void Settings::setSkipUppercase(bool skip)
{
if (!d->loader->settings()->setCheckUppercase(!skip)) {
return;
}
Q_EMIT skipUppercaseChanged();
Q_EMIT modifiedChanged();
}
bool Settings::skipUppercase() const
{
return !d->loader->settings()->checkUppercase();
}
void Settings::setAutodetectLanguage(bool detect)
{
if (!d->loader->settings()->setAutodetectLanguage(detect)) {
return;
}
Q_EMIT autodetectLanguageChanged();
Q_EMIT modifiedChanged();
}
bool Settings::autodetectLanguage() const
{
return d->loader->settings()->autodetectLanguage();
}
void Settings::setSkipRunTogether(bool skip)
{
if (skipRunTogether() == skip) {
return;
}
d->loader->settings()->setSkipRunTogether(skip);
Q_EMIT skipRunTogetherChanged();
Q_EMIT modifiedChanged();
}
bool Settings::skipRunTogether() const
{
return d->loader->settings()->skipRunTogether();
}
void Settings::setCheckerEnabledByDefault(bool check)
{
if (checkerEnabledByDefault() == check) {
return;
}
d->loader->settings()->setCheckerEnabledByDefault(check);
Q_EMIT checkerEnabledByDefaultChanged();
Q_EMIT modifiedChanged();
}
bool Settings::checkerEnabledByDefault() const
{
return d->loader->settings()->checkerEnabledByDefault();
}
void Settings::setBackgroundCheckerEnabled(bool enable)
{
if (backgroundCheckerEnabled() == enable) {
return;
}
d->loader->settings()->setBackgroundCheckerEnabled(enable);
Q_EMIT backgroundCheckerEnabledChanged();
Q_EMIT modifiedChanged();
}
bool Settings::backgroundCheckerEnabled() const
{
return d->loader->settings()->backgroundCheckerEnabled();
}
void Settings::setCurrentIgnoreList(const QStringList &ignores)
{
if (currentIgnoreList() == ignores) {
return;
}
d->loader->settings()->setCurrentIgnoreList(ignores);
Q_EMIT currentIgnoreListChanged();
Q_EMIT modifiedChanged();
}
QStringList Settings::currentIgnoreList() const
{
return d->loader->settings()->currentIgnoreList();
}
QStringList Settings::clients() const
{
return d->loader->clients();
}
void Settings::save()
{
d->loader->settings()->save();
Q_EMIT modifiedChanged();
}
bool Settings::modified() const
{
return d->loader->settings()->modified();
}
// default values
// A static list of KDE specific words that we want to recognize
QStringList Settings::defaultIgnoreList()
{
QStringList l;
l.append(QStringLiteral("KMail"));
l.append(QStringLiteral("KOrganizer"));
l.append(QStringLiteral("KAddressBook"));
l.append(QStringLiteral("KHTML"));
l.append(QStringLiteral("KIO"));
l.append(QStringLiteral("KJS"));
l.append(QStringLiteral("Konqueror"));
l.append(QStringLiteral("Sonnet"));
l.append(QStringLiteral("Kontact"));
l.append(QStringLiteral("Qt"));
l.append(QStringLiteral("Okular"));
l.append(QStringLiteral("KMix"));
l.append(QStringLiteral("Amarok"));
l.append(QStringLiteral("KDevelop"));
l.append(QStringLiteral("Nepomuk"));
return l;
}
bool Settings::defaultSkipUppercase()
{
return false;
}
bool Settings::defaultAutodetectLanguage()
{
return true;
}
bool Settings::defaultBackgroundCheckerEnabled()
{
return true;
}
bool Settings::defaultCheckerEnabledByDefault()
{
return false;
}
bool Settings::defauktSkipRunTogether()
{
return true;
}
QString Settings::defaultDefaultLanguage()
{
return QLocale::system().name();
}
QStringList Settings::defaultPreferredLanguages()
{
return QStringList();
}
QAbstractListModel *Settings::dictionaryModel()
{
// Lazy loading
if (d->dictionaryModel) {
return d->dictionaryModel;
}
d->dictionaryModel = new DictionaryModel(this);
d->dictionaryModel->setDefaultLanguage(defaultLanguage());
return d->dictionaryModel;
}
}
#include "moc_settings.cpp"
#include "settings.moc"
@@ -0,0 +1,124 @@
/*
* SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_SETTINGS_H
#define SONNET_SETTINGS_H
#include <QAbstractListModel>
#include <QObject>
#include <QString>
#include <QStringList>
#include "sonnetcore_export.h"
#include <memory>
namespace Sonnet
{
class Loader;
class SettingsPrivate;
class SONNETCORE_EXPORT Settings : public QObject
{
Q_OBJECT
/// This property holds whether Sonnet should skip checkign words starting with an uppercase letter.
Q_PROPERTY(bool skipUppercase READ skipUppercase WRITE setSkipUppercase NOTIFY skipUppercaseChanged)
/// This property holds whether Sonnet should autodetect language.
Q_PROPERTY(bool autodetectLanguage READ autodetectLanguage WRITE setAutodetectLanguage NOTIFY autodetectLanguageChanged)
/// This property holds whether Sonnet should run spellchecking checks in the background.
Q_PROPERTY(bool backgroundCheckerEnabled READ backgroundCheckerEnabled WRITE setBackgroundCheckerEnabled NOTIFY backgroundCheckerEnabledChanged)
/// This property holds whether Sonnet should be enabled by default.
Q_PROPERTY(bool checkerEnabledByDefault READ checkerEnabledByDefault WRITE setCheckerEnabledByDefault NOTIFY checkerEnabledByDefaultChanged)
/// This property holds whether Sonnet should skip checking compounds words.
Q_PROPERTY(bool skipRunTogether READ skipRunTogether WRITE setSkipRunTogether NOTIFY skipRunTogetherChanged)
/// This property holds the list of ignored words.
Q_PROPERTY(QStringList currentIgnoreList READ currentIgnoreList WRITE setCurrentIgnoreList NOTIFY currentIgnoreListChanged)
/// This property holds the list of preferred languages.
Q_PROPERTY(QStringList preferredLanguages READ preferredLanguages WRITE setPreferredLanguages NOTIFY preferredLanguagesChanged)
/// This property holds the default language for spell checking.
Q_PROPERTY(QString defaultLanguage READ defaultLanguage WRITE setDefaultLanguage NOTIFY defaultLanguageChanged)
/// This property holds a Qt Model containing all the preferred dictionaries
/// with language description and theirs codes. This model makes the
/// Qt::DisplayRole as well as the roles defined in \ref DictionaryRoles
/// available.
/// \since 5.88
Q_PROPERTY(QAbstractListModel *dictionaryModel READ dictionaryModel CONSTANT)
Q_PROPERTY(bool modified READ modified NOTIFY modifiedChanged)
public:
/// Roles for \ref dictionaryModel
enum DictionaryRoles {
LanguageCodeRole = Qt::UserRole + 1, //< Language code of the language. This uses "languageCode" as roleNames.
PreferredRole, //< This role holds whether the language is one of the preferred languages. This uses "isPreferred" as roleNames.
DefaultRole //< This role holds whether the language is the default language. This uses "isDefault" as roleNames.
};
explicit Settings(QObject *parent = nullptr);
~Settings() override;
void setDefaultLanguage(const QString &lang);
QString defaultLanguage() const;
void setPreferredLanguages(const QStringList &lang);
QStringList preferredLanguages() const;
void setDefaultClient(const QString &client);
QString defaultClient() const;
void setSkipUppercase(bool);
bool skipUppercase() const;
void setAutodetectLanguage(bool);
bool autodetectLanguage() const;
void setSkipRunTogether(bool);
bool skipRunTogether() const;
void setBackgroundCheckerEnabled(bool);
bool backgroundCheckerEnabled() const;
void setCheckerEnabledByDefault(bool);
bool checkerEnabledByDefault() const;
void setCurrentIgnoreList(const QStringList &ignores);
QStringList currentIgnoreList() const;
QStringList clients() const;
bool modified() const;
QAbstractListModel *dictionaryModel();
Q_INVOKABLE void save();
static QStringList defaultIgnoreList();
static bool defaultSkipUppercase();
static bool defaultAutodetectLanguage();
static bool defaultBackgroundCheckerEnabled();
static bool defaultCheckerEnabledByDefault();
static bool defauktSkipRunTogether();
static QString defaultDefaultLanguage();
static QStringList defaultPreferredLanguages();
Q_SIGNALS:
void skipUppercaseChanged();
void autodetectLanguageChanged();
void backgroundCheckerEnabledChanged();
void defaultClientChanged();
void defaultLanguageChanged();
void preferredLanguagesChanged();
void skipRunTogetherChanged();
void checkerEnabledByDefaultChanged();
void currentIgnoreListChanged();
void modifiedChanged();
private:
friend class Loader;
std::unique_ptr<SettingsPrivate> const d;
};
}
#endif // SONNET_SETTINGS_H
@@ -0,0 +1,278 @@
/*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2006 Laurent Montel <montel@kde.org>
* SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "settingsimpl_p.h"
#include "loader_p.h"
#include <QMap>
#include <QSettings>
#include "settings.h"
namespace Sonnet
{
class SettingsImplPrivate
{
public:
Loader *loader = nullptr; // can't be a Ptr since we don't want to hold a ref on it
bool modified = false;
QString defaultLanguage;
QStringList preferredLanguages;
QString defaultClient;
bool checkUppercase = false;
bool skipRunTogether = false;
bool backgroundCheckerEnabled = false;
bool checkerEnabledByDefault = false;
bool autodetectLanguage = false;
int disablePercentage;
int disableWordCount;
QMap<QString, bool> ignore;
};
SettingsImpl::SettingsImpl(Loader *loader)
: d(new SettingsImplPrivate)
{
d->loader = loader;
d->modified = false;
d->checkerEnabledByDefault = false;
restore();
}
SettingsImpl::~SettingsImpl() = default;
bool SettingsImpl::setDefaultLanguage(const QString &lang)
{
const QStringList cs = d->loader->languages();
if (cs.indexOf(lang) != -1 && d->defaultLanguage != lang) {
d->defaultLanguage = lang;
d->modified = true;
d->loader->changed();
return true;
}
return false;
}
QString SettingsImpl::defaultLanguage() const
{
return d->defaultLanguage;
}
bool SettingsImpl::setPreferredLanguages(const QStringList &lang)
{
if (d->preferredLanguages != lang) {
d->modified = true;
d->preferredLanguages = lang;
return true;
}
return false;
}
QStringList SettingsImpl::preferredLanguages() const
{
return d->preferredLanguages;
}
bool SettingsImpl::setDefaultClient(const QString &client)
{
// Different from setDefaultLanguage because
// the number of clients can't be even close
// as big as the number of languages
if (d->loader->clients().contains(client)) {
d->defaultClient = client;
d->modified = true;
d->loader->changed();
return true;
}
return false;
}
QString SettingsImpl::defaultClient() const
{
return d->defaultClient;
}
bool SettingsImpl::setCheckUppercase(bool check)
{
if (d->checkUppercase != check) {
d->modified = true;
d->checkUppercase = check;
return true;
}
return false;
}
bool SettingsImpl::checkUppercase() const
{
return d->checkUppercase;
}
bool SettingsImpl::setAutodetectLanguage(bool detect)
{
if (d->autodetectLanguage != detect) {
d->modified = true;
d->autodetectLanguage = detect;
return true;
}
return false;
}
bool SettingsImpl::autodetectLanguage() const
{
return d->autodetectLanguage;
}
bool SettingsImpl::setSkipRunTogether(bool skip)
{
if (d->skipRunTogether != skip) {
d->modified = true;
d->skipRunTogether = skip;
return true;
}
return false;
}
bool SettingsImpl::skipRunTogether() const
{
return d->skipRunTogether;
}
bool SettingsImpl::setCheckerEnabledByDefault(bool check)
{
if (d->checkerEnabledByDefault != check) {
d->modified = true;
d->checkerEnabledByDefault = check;
return true;
}
return false;
}
bool SettingsImpl::checkerEnabledByDefault() const
{
return d->checkerEnabledByDefault;
}
bool SettingsImpl::setBackgroundCheckerEnabled(bool enable)
{
if (d->backgroundCheckerEnabled != enable) {
d->modified = true;
d->backgroundCheckerEnabled = enable;
return true;
}
return false;
}
bool SettingsImpl::backgroundCheckerEnabled() const
{
return d->backgroundCheckerEnabled;
}
bool SettingsImpl::setCurrentIgnoreList(const QStringList &ignores)
{
bool changed = setQuietIgnoreList(ignores);
d->modified = true;
return changed;
}
bool SettingsImpl::setQuietIgnoreList(const QStringList &ignores)
{
bool changed = false;
d->ignore = QMap<QString, bool>(); // clear out
for (QStringList::const_iterator itr = ignores.begin(); itr != ignores.end(); ++itr) {
d->ignore.insert(*itr, true);
changed = true;
}
return changed;
}
QStringList SettingsImpl::currentIgnoreList() const
{
return d->ignore.keys();
}
bool SettingsImpl::addWordToIgnore(const QString &word)
{
if (!d->ignore.contains(word)) {
d->modified = true;
d->ignore.insert(word, true);
return true;
}
return false;
}
bool SettingsImpl::ignore(const QString &word)
{
return d->ignore.contains(word);
}
int SettingsImpl::disablePercentageWordError() const
{
return d->disablePercentage;
}
int SettingsImpl::disableWordErrorCount() const
{
return d->disableWordCount;
}
void SettingsImpl::save()
{
QSettings settings(QStringLiteral("KDE"), QStringLiteral("Sonnet"));
settings.setValue(QStringLiteral("defaultClient"), d->defaultClient);
settings.setValue(QStringLiteral("defaultLanguage"), d->defaultLanguage);
settings.setValue(QStringLiteral("preferredLanguages"), d->preferredLanguages);
settings.setValue(QStringLiteral("checkUppercase"), d->checkUppercase);
settings.setValue(QStringLiteral("skipRunTogether"), d->skipRunTogether);
settings.setValue(QStringLiteral("backgroundCheckerEnabled"), d->backgroundCheckerEnabled);
settings.setValue(QStringLiteral("checkerEnabledByDefault"), d->checkerEnabledByDefault);
settings.setValue(QStringLiteral("autodetectLanguage"), d->autodetectLanguage);
QString defaultLanguage = QStringLiteral("ignore_%1").arg(d->defaultLanguage);
if (settings.contains(defaultLanguage) && d->ignore.isEmpty()) {
settings.remove(defaultLanguage);
} else if (!d->ignore.isEmpty()) {
settings.setValue(defaultLanguage, QStringList(d->ignore.keys()));
}
d->modified = false;
}
void SettingsImpl::restore()
{
QSettings settings(QStringLiteral("KDE"), QStringLiteral("Sonnet"));
d->defaultClient = settings.value(QStringLiteral("defaultClient"), QString()).toString();
d->defaultLanguage = settings.value(QStringLiteral("defaultLanguage"), Settings::defaultDefaultLanguage()).toString();
d->preferredLanguages = settings.value(QStringLiteral("preferredLanguages"), Settings::defaultPreferredLanguages()).toStringList();
// same defaults are in the default filter (filter.cpp)
d->checkUppercase = settings.value(QStringLiteral("checkUppercase"), !Settings::defaultSkipUppercase()).toBool();
d->skipRunTogether = settings.value(QStringLiteral("skipRunTogether"), Settings::defauktSkipRunTogether()).toBool();
d->backgroundCheckerEnabled = settings.value(QStringLiteral("backgroundCheckerEnabled"), Settings::defaultBackgroundCheckerEnabled()).toBool();
d->checkerEnabledByDefault = settings.value(QStringLiteral("checkerEnabledByDefault"), Settings::defaultCheckerEnabledByDefault()).toBool();
d->disablePercentage = settings.value(QStringLiteral("Sonnet_AsYouTypeDisablePercentage"), 90).toInt();
d->disableWordCount = settings.value(QStringLiteral("Sonnet_AsYouTypeDisableWordCount"), 100).toInt();
d->autodetectLanguage = settings.value(QStringLiteral("autodetectLanguage"), Settings::defaultAutodetectLanguage()).toBool();
const QString ignoreEntry = QStringLiteral("ignore_%1").arg(d->defaultLanguage);
const QStringList ignores = settings.value(ignoreEntry, Settings::defaultIgnoreList()).toStringList();
setQuietIgnoreList(ignores);
}
bool SettingsImpl::modified() const
{
return d->modified;
}
void SettingsImpl::setModified(bool modified)
{
d->modified = modified;
}
} // namespace Sonnet
@@ -0,0 +1,81 @@
/*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_SETTINGS_IMPL_P_H
#define SONNET_SETTINGS_IMPL_P_H
#include "sonnetcore_export.h"
#include <QString>
#include <QStringList>
#include <memory>
namespace Sonnet
{
class Loader;
class SettingsImplPrivate;
/**
* SettingsImpl class
*/
class SONNETCORE_EXPORT SettingsImpl
{
public:
~SettingsImpl();
SettingsImpl(const SettingsImpl &) = delete;
SettingsImpl &operator=(const SettingsImpl &) = delete;
bool modified() const;
void setModified(bool modified);
bool setDefaultLanguage(const QString &lang);
QString defaultLanguage() const;
bool setPreferredLanguages(const QStringList &lang);
QStringList preferredLanguages() const;
bool setDefaultClient(const QString &client);
QString defaultClient() const;
bool setCheckUppercase(bool);
bool checkUppercase() const;
bool setAutodetectLanguage(bool);
bool autodetectLanguage() const;
bool setSkipRunTogether(bool);
bool skipRunTogether() const;
bool setBackgroundCheckerEnabled(bool);
bool backgroundCheckerEnabled() const;
bool setCheckerEnabledByDefault(bool);
bool checkerEnabledByDefault() const;
bool setCurrentIgnoreList(const QStringList &ignores);
bool addWordToIgnore(const QString &word);
QStringList currentIgnoreList() const;
bool ignore(const QString &word);
void save();
void restore();
int disablePercentageWordError() const;
int disableWordErrorCount() const;
private:
SONNETCORE_NO_EXPORT bool setQuietIgnoreList(const QStringList &ignores);
private:
friend class Loader;
SONNETCORE_NO_EXPORT explicit SettingsImpl(Loader *loader);
private:
std::unique_ptr<SettingsImplPrivate> const d;
};
}
#endif // SONNET_SETTINGS_IMPL_P_H
@@ -0,0 +1,26 @@
TARGET = sonnet-core
TEMPLATE = lib
CONFIG += staticlib c++11
QT -= gui
SOURCES += loader.cpp \
client.cpp \
spellerplugin.cpp \
speller.cpp \
settings.cpp \
backgroundchecker.cpp \
guesslanguage.cpp \
textbreaks.cpp \
tokenizer.cpp \
languagefilter.cpp
HEADERS += client_p.h \
loader_p.h
DEFINES += SONNETCORE_EXPORT=""
DEFINES += INSTALLATION_PLUGIN_PATH=""
DEFINES += SONNET_STATIC
unix:system("touch sonnetcore_export.h")
win32:system("type nul > sonnetcore_export.h")
@@ -0,0 +1,274 @@
/*
* SPDX-FileCopyrightText: 2007 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "speller.h"
#include "loader_p.h"
#include "settingsimpl_p.h"
#include "spellerplugin_p.h"
#include "core_debug.h"
#include <QCache>
namespace Sonnet
{
class SpellerPrivate
{
public:
SpellerPrivate()
{
}
~SpellerPrivate()
{
}
void init(const QString &lang)
{
Loader *loader = Loader::openLoader();
settings = loader->settings();
language = lang;
updateDict();
}
void updateDict()
{
dict = Loader::openLoader()->cachedSpeller(language);
}
bool isValid()
{
if (settings->modified()) {
recreateDict();
settings->setModified(false);
}
return !dict.isNull();
}
void recreateDict()
{
Loader::openLoader()->clearSpellerCache();
updateDict();
}
QSharedPointer<SpellerPlugin> dict;
SettingsImpl *settings = nullptr;
QString language;
};
Speller::Speller(const QString &lang)
: d(new SpellerPrivate)
{
d->init(lang);
}
Speller::~Speller()
{
qCDebug(SONNET_LOG_CORE) << "deleting" << this << "for" << d->language;
}
Speller::Speller(const Speller &speller)
: d(new SpellerPrivate)
{
d->language = speller.language();
d->init(d->language);
}
Speller &Speller::operator=(const Speller &speller)
{
d->language = speller.language();
d->updateDict();
return *this;
}
bool Speller::isCorrect(const QString &word) const
{
if (!d->isValid()) {
return true;
}
return d->dict->isCorrect(word);
}
bool Speller::isMisspelled(const QString &word) const
{
if (!d->isValid()) {
return false;
}
return d->dict->isMisspelled(word);
}
QStringList Speller::suggest(const QString &word) const
{
if (!d->isValid()) {
return QStringList();
}
return d->dict->suggest(word);
}
bool Speller::checkAndSuggest(const QString &word, QStringList &suggestions) const
{
if (!d->isValid()) {
return true;
}
return d->dict->checkAndSuggest(word, suggestions);
}
bool Speller::storeReplacement(const QString &bad, const QString &good)
{
if (!d->isValid()) {
return false;
}
return d->dict->storeReplacement(bad, good);
}
bool Speller::addToPersonal(const QString &word)
{
if (!d->isValid()) {
return false;
}
return d->dict->addToPersonal(word);
}
bool Speller::addToSession(const QString &word)
{
if (!d->isValid()) {
return false;
}
return d->dict->addToSession(word);
}
QString Speller::language() const
{
if (!d->isValid()) {
return QString();
}
return d->dict->language();
}
void Speller::save()
{
if (d->settings) {
d->settings->save();
}
}
void Speller::restore()
{
if (d->settings) {
d->settings->restore();
d->recreateDict();
}
}
QStringList Speller::availableBackends() const
{
Loader *l = Loader::openLoader();
return l->clients();
}
QStringList Speller::availableLanguages() const
{
Loader *l = Loader::openLoader();
return l->languages();
}
QStringList Speller::availableLanguageNames() const
{
Loader *l = Loader::openLoader();
return l->languageNames();
}
void Speller::setDefaultLanguage(const QString &lang)
{
if (d->settings->setDefaultLanguage(lang)) {
d->settings->save();
}
}
QString Speller::defaultLanguage() const
{
return d->settings->defaultLanguage();
}
void Speller::setDefaultClient(const QString &client)
{
if (d->settings->setDefaultClient(client)) {
d->settings->save();
}
}
QString Speller::defaultClient() const
{
return d->settings->defaultClient();
}
void Speller::setAttribute(Attribute attr, bool b)
{
switch (attr) {
case CheckUppercase:
d->settings->setCheckUppercase(b);
break;
case SkipRunTogether:
d->settings->setSkipRunTogether(b);
break;
case AutoDetectLanguage:
d->settings->setAutodetectLanguage(b);
break;
}
d->settings->save();
}
bool Speller::testAttribute(Attribute attr) const
{
switch (attr) {
case CheckUppercase:
return d->settings->checkUppercase();
case SkipRunTogether:
return d->settings->skipRunTogether();
case AutoDetectLanguage:
return d->settings->autodetectLanguage();
}
return false;
}
bool Speller::isValid() const
{
return !d->dict.isNull();
}
void Speller::setLanguage(const QString &lang)
{
d->language = lang;
d->updateDict();
}
QMap<QString, QString> Sonnet::Speller::availableDictionaries() const
{
Loader *l = Loader::openLoader();
const QStringList lst = l->languages();
QMap<QString, QString> langs;
for (const QString &tag : lst) {
langs.insert(l->languageNameForCode(tag), tag);
}
return langs;
}
QMap<QString, QString> Speller::preferredDictionaries() const
{
Loader *l = Loader::openLoader();
QMap<QString, QString> langs;
for (const QString &tag : l->settings()->preferredLanguages()) {
langs.insert(l->languageNameForCode(tag), tag);
}
return langs;
}
} // namespace Sonnet
@@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2007 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_SPELLER_H
#define SONNET_SPELLER_H
#include <QMap>
#include <QString>
#include <QStringList>
#include "sonnetcore_export.h"
#include <memory>
namespace Sonnet
{
class SpellerPrivate;
/**
* @class Sonnet::Speller speller.h <Sonnet/Speller>
*
* Spell checker object.
*
* @short class used for actual spell checking
*/
class SONNETCORE_EXPORT Speller
{
public:
explicit Speller(const QString &lang = QString());
~Speller();
Speller(const Speller &speller);
Speller &operator=(const Speller &speller);
/**
* @return @c true if the speller supports currently selected
* language.
*/
bool isValid() const;
/**
* Sets the language supported by this speller.
*/
void setLanguage(const QString &lang);
/**
* @return language supported by this speller.
*/
QString language() const;
/**
* Checks the given word.
* @return false if the word is misspelled. true otherwise
*/
bool isCorrect(const QString &word) const;
/**
* Checks the given word.
* @return true if the word is misspelled. false otherwise
*/
bool isMisspelled(const QString &word) const;
/**
* Fetches suggestions for the word.
*
* @return list of all suggestions for the word
*/
QStringList suggest(const QString &word) const;
/**
* Convenience method calling isCorrect() and suggest()
* if the word isn't correct.
*/
bool checkAndSuggest(const QString &word, QStringList &suggestions) const;
/**
* Stores user defined good replacement for the bad word.
* @return @c true on success
*/
bool storeReplacement(const QString &bad, const QString &good);
/**
* Adds word to the list of of personal words.
* @return true on success
*/
bool addToPersonal(const QString &word);
/**
* Adds word to the words recognizable in the current session.
* @return true on success
*/
bool addToSession(const QString &word);
public: // Configuration API
enum Attribute {
CheckUppercase,
SkipRunTogether,
AutoDetectLanguage,
};
void save();
void restore();
/**
* @return names of all supported backends (e.g. ISpell, ASpell)
*/
QStringList availableBackends() const;
/**
* @return a list of supported languages.
*
* Note: use availableDictionaries
*/
QStringList availableLanguages() const;
/**
* @return a localized list of names of supported languages.
*
* Note: use availableDictionaries
*/
QStringList availableLanguageNames() const;
/**
* @return a map of all available dictionaries with language descriptions and
* their codes. The key is the description, the code the value.
*/
QMap<QString, QString> availableDictionaries() const;
/**
* @return a map of user preferred dictionaries with language descriptions and
* their codes. The key is the description, the code the value.
* @since 5.54
*/
QMap<QString, QString> preferredDictionaries() const;
void setDefaultLanguage(const QString &lang);
QString defaultLanguage() const;
void setDefaultClient(const QString &client);
QString defaultClient() const;
void setAttribute(Attribute attr, bool b = true);
bool testAttribute(Attribute attr) const;
private:
std::unique_ptr<SpellerPrivate> const d;
};
}
#endif
@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2006 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "spellerplugin_p.h"
namespace Sonnet
{
class SpellerPluginPrivate
{
public:
QString language;
};
SpellerPlugin::SpellerPlugin(const QString &lang)
: d(new SpellerPluginPrivate)
{
d->language = lang;
}
SpellerPlugin::~SpellerPlugin() = default;
QString SpellerPlugin::language() const
{
return d->language;
}
bool SpellerPlugin::isMisspelled(const QString &word) const
{
return !isCorrect(word);
}
bool SpellerPlugin::checkAndSuggest(const QString &word, QStringList &suggestions) const
{
bool c = isCorrect(word);
if (!c) {
suggestions = suggest(word);
}
return c;
}
}
@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_SPELLERPLUGIN_P_H
#define SONNET_SPELLERPLUGIN_P_H
#include <QString>
#include <QStringList>
#include "sonnetcore_export.h"
#include <memory>
namespace Sonnet
{
class SpellerPluginPrivate;
/**
* Class is returned by from Loader. It acts
* as the actual spellchecker.
*
* @author Zack Rusin <zack@kde.org>
* @short class used for actual spell checking
*/
class SONNETCORE_EXPORT SpellerPlugin
{
public:
virtual ~SpellerPlugin();
/**
* Checks the given word.
* @return false if the word is misspelled. true otherwise
*/
virtual bool isCorrect(const QString &word) const = 0;
/**
* Checks the given word.
* @return true if the word is misspelled. false otherwise
*/
bool isMisspelled(const QString &word) const;
/**
* Fetches suggestions for the word.
*
* @return list of all suggestions for the word
*/
virtual QStringList suggest(const QString &word) const = 0;
/**
* Convenient method calling isCorrect() and suggest()
* if the word isn't correct.
*/
virtual bool checkAndSuggest(const QString &word, QStringList &suggestions) const;
/**
* Stores user defined good replacement for the bad word.
* @returns true on success
*/
virtual bool storeReplacement(const QString &bad, const QString &good) = 0;
/**
* Adds word to the list of of personal words.
* @return true on success
*/
virtual bool addToPersonal(const QString &word) = 0;
/**
* Adds word to the words recognizable in the current session.
* @return true on success
*/
virtual bool addToSession(const QString &word) = 0;
/**
* Returns language supported by this dictionary.
*/
QString language() const;
protected:
SpellerPlugin(const QString &lang);
private:
std::unique_ptr<SpellerPluginPrivate> const d;
};
}
#endif
@@ -0,0 +1,116 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2006 Jacob R Rideout <kde@jacobrideout.net>
SPDX-FileCopyrightText: 2006 Martin Sandsmark <martin.sandsmark@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QHash>
#include <QString>
#include <QTextBoundaryFinder>
#include "textbreaks_p.h"
namespace Sonnet
{
class TextBreaksPrivate
{
public:
TextBreaksPrivate()
{
}
QString text;
};
TextBreaks::TextBreaks(const QString &text)
: d(new TextBreaksPrivate())
{
setText(text);
}
TextBreaks::~TextBreaks() = default;
QString TextBreaks::text() const
{
return d->text;
}
void TextBreaks::setText(const QString &text)
{
d->text = text;
}
TextBreaks::Positions TextBreaks::wordBreaks(const QString &text)
{
Positions breaks;
if (text.isEmpty()) {
return breaks;
}
QTextBoundaryFinder boundaryFinder(QTextBoundaryFinder::Word, text);
while (boundaryFinder.position() < text.length()) {
if (!(boundaryFinder.boundaryReasons().testFlag(QTextBoundaryFinder::StartOfItem))) {
if (boundaryFinder.toNextBoundary() == -1) {
break;
}
continue;
}
Position pos;
pos.start = boundaryFinder.position();
int end = boundaryFinder.toNextBoundary();
if (end == -1) {
break;
}
pos.length = end - pos.start;
if (pos.length < 1) {
continue;
}
breaks.append(pos);
if (boundaryFinder.toNextBoundary() == -1) {
break;
}
}
return breaks;
}
TextBreaks::Positions TextBreaks::sentenceBreaks(const QString &text)
{
Positions breaks;
if (text.isEmpty()) {
return breaks;
}
QTextBoundaryFinder boundaryFinder(QTextBoundaryFinder::Sentence, text);
while (boundaryFinder.position() < text.length()) {
Position pos;
pos.start = boundaryFinder.position();
int end = boundaryFinder.toNextBoundary();
if (end == -1) {
break;
}
pos.length = end - pos.start;
if (pos.length < 1) {
continue;
}
breaks.append(pos);
}
return breaks;
}
TextBreaks::Positions TextBreaks::wordBreaks() const
{
return wordBreaks(d->text);
}
TextBreaks::Positions TextBreaks::sentenceBreaks() const
{
return sentenceBreaks(d->text);
}
}
@@ -0,0 +1,101 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2006 Jacob R Rideout <kde@jacobrideout.net>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef TEXTBREAKS_H
#define TEXTBREAKS_H
class QString;
#include "sonnetcore_export.h"
#include <memory>
namespace Sonnet
{
class TextBreaksPrivate;
/**
* @short TextBreaks determines the barriers between linguistic structures in any given text.
*
* TextBreaks is a class that determines the boundaries between graphemes
* (characters as per the unicode definition,) words and sentences. The
* default implementation conforms to Unicode Standard Annex #29 https://unicode.org/reports/tr29/.
* You can subclass TextBreaks to create the correct behaviour for languages that require it.
*
* @author Jacob Rideout <kde@jacobrideout.net>
* @since 4.3
*/
class SONNETCORE_EXPORT TextBreaks
{
public:
struct Position {
int start, length;
};
/**
* This structure abstracts the positions of breaks in the test. As per the
* unicode annex, both the start and end of the text are returned.
*/
typedef QList<Position> Positions;
/** Constructor
* Creates a new TextBreaks instance. If @p text is specified,
* it sets the text to be checked.
* @param text the text that is to be checked
*/
explicit TextBreaks(const QString &text = QString());
/** Virtual Destructor
*/
virtual ~TextBreaks();
/**
* Returns the text to be checked
* @return text
*/
QString text() const;
/**
* Sets the text to @p text
* @param text to be set
* @return true if the word is misspelled. false otherwise
*/
void setText(const QString &text);
/**
* Return the Positions of each word for the given @p text.
* @param text to be checked
* @return positions of breaks
*/
static Positions wordBreaks(const QString &text);
/**
* Return the Positions of each sentence for the given @p text.
* @param text to be checked
* @return positions of breaks
*/
static Positions sentenceBreaks(const QString &text);
/**
* Return the Positions of each word for the text previously set.
* @return positions of breaks
*/
virtual Positions wordBreaks() const;
/**
* Return the Positions of each sentence for the text previously set.
* @return positions of breaks
*/
virtual Positions sentenceBreaks() const;
private:
std::unique_ptr<TextBreaksPrivate> const d;
};
}
Q_DECLARE_TYPEINFO(Sonnet::TextBreaks::Position, Q_PRIMITIVE_TYPE);
#endif
@@ -0,0 +1,265 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
SPDX-FileCopyrightText: 2006 Jacob R Rideout <kde@jacobrideout.net>
SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QList>
#include <QString>
#include "textbreaks_p.h"
#include "tokenizer_p.h"
namespace Sonnet
{
class BreakTokenizerPrivate
{
public:
enum Type {
Words,
Sentences,
};
BreakTokenizerPrivate(Type s)
: breakFinder(new TextBreaks)
, itemPosition(-1)
, cacheValid(false)
, type(s)
{
}
~BreakTokenizerPrivate()
{
delete breakFinder;
}
TextBreaks::Positions breaks() const;
void invalidate();
void shiftBreaks(int from, int offset);
void replace(int pos, int len, const QString &newWord);
TextBreaks *const breakFinder;
QString buffer;
int itemPosition = -1;
mutable bool cacheValid;
Token last;
const Type type;
bool inAddress = false;
bool ignoreUppercase = false;
bool hasNext() const;
Token next();
void setBuffer(const QString &b)
{
invalidate();
buffer = b;
}
private:
void regenerateCache() const;
mutable TextBreaks::Positions cachedBreaks;
};
void BreakTokenizerPrivate::invalidate()
{
cacheValid = false;
itemPosition = -1;
}
bool BreakTokenizerPrivate::hasNext() const
{
if (itemPosition >= (breaks().size() - 1)) {
return false;
}
return true;
}
TextBreaks::Positions BreakTokenizerPrivate::breaks() const
{
if (!cacheValid) {
regenerateCache();
}
return cachedBreaks;
}
void BreakTokenizerPrivate::shiftBreaks(int from, int offset)
{
for (int i = 0; i < cachedBreaks.size(); i++) {
if (cachedBreaks[i].start > from) {
cachedBreaks[i].start = cachedBreaks[i].start - offset;
}
}
}
void BreakTokenizerPrivate::regenerateCache() const
{
if (!breakFinder || buffer.isEmpty()) {
cachedBreaks = TextBreaks::Positions();
}
if (breakFinder) {
breakFinder->setText(buffer);
if (type == Sentences) {
cachedBreaks = breakFinder->sentenceBreaks();
} else if (type == Words) {
cachedBreaks = breakFinder->wordBreaks();
}
}
cacheValid = true;
}
Token BreakTokenizerPrivate::next()
{
Token block;
if (!hasNext()) {
last = block;
return block;
}
itemPosition++;
const TextBreaks::Positions breaks = this->breaks();
const TextBreaks::Position &textBreak = breaks.at(itemPosition);
QStringView token = QStringView(buffer).mid(textBreak.start, textBreak.length);
last = {token, textBreak.start};
return last;
}
void BreakTokenizerPrivate::replace(int pos, int len, const QString &newWord)
{
buffer.replace(pos, len, newWord);
int offset = len - newWord.length();
if (cacheValid) {
shiftBreaks(pos, offset);
}
}
/*-----------------------------------------------------------*/
WordTokenizer::WordTokenizer(const QString &buffer)
: d(new BreakTokenizerPrivate(BreakTokenizerPrivate::Words))
{
setBuffer(buffer);
}
WordTokenizer::~WordTokenizer() = default;
bool WordTokenizer::hasNext() const
{
return d->hasNext();
}
void WordTokenizer::setBuffer(const QString &buffer)
{
d->setBuffer(buffer);
}
Token WordTokenizer::next()
{
Token n = d->next();
// end of address of url?
if (d->inAddress && n.position() > 0 && d->buffer[n.position() - 1].isSpace()) {
d->inAddress = false;
}
// check if this word starts an email address of url
if (!d->inAddress || hasNext()) {
const int pos = n.position() + n.length();
if ((pos < d->buffer.length()) && d->buffer[pos] == QLatin1Char('@')) {
d->inAddress = true;
}
if ((pos + 2 < d->buffer.length()) && d->buffer[pos] == QLatin1Char(':') && d->buffer[pos + 1] == QLatin1Char('/')
&& d->buffer[pos + 2] == QLatin1Char('/')) {
d->inAddress = true;
}
}
return n;
}
QString WordTokenizer::buffer() const
{
return d->buffer;
}
bool WordTokenizer::isUppercase(QStringView word) const
{
for (int i = 0; i < word.length(); ++i) {
if (word.at(i).isLetter() && !word.at(i).isUpper()) {
return false;
}
}
return true;
}
void WordTokenizer::setIgnoreUppercase(bool val)
{
d->ignoreUppercase = val;
}
void WordTokenizer::replace(int pos, int len, const QString &newWord)
{
d->replace(pos, len, newWord);
}
bool WordTokenizer::isSpellcheckable() const
{
if (d->last.isNull() || d->last.isEmpty()) {
return false;
}
if (!d->last.at(0).isLetter()) {
return false;
}
if (d->inAddress) {
return false;
}
if (d->ignoreUppercase && isUppercase(d->last.token)) {
return false;
}
return true;
}
/* --------------------------------------------------------------------*/
SentenceTokenizer::SentenceTokenizer(const QString &buffer)
: d(new BreakTokenizerPrivate(BreakTokenizerPrivate::Sentences))
{
setBuffer(buffer);
}
SentenceTokenizer::~SentenceTokenizer() = default;
bool SentenceTokenizer::hasNext() const
{
return d->hasNext();
}
void SentenceTokenizer::setBuffer(const QString &buffer)
{
d->setBuffer(buffer);
}
Token SentenceTokenizer::next()
{
return d->next();
}
QString SentenceTokenizer::buffer() const
{
return d->buffer;
}
void SentenceTokenizer::replace(int pos, int len, const QString &newWord)
{
d->replace(pos, len, newWord);
}
}
@@ -0,0 +1,173 @@
/* This file is part of the KDE libraries
SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef ABSTRACTTOKENIZER_H
#define ABSTRACTTOKENIZER_H
#include "sonnetcore_export.h"
#include <QString>
#include <memory>
namespace Sonnet
{
struct Token {
QStringView token = nullptr;
int positionInBuffer = -1;
QString toString() const
{
return token.toString();
}
/**
* @brief length of this token
*/
Q_DECL_CONSTEXPR int length() const
{
return token.size();
}
/**
* @brief position in buffer of which the @ref token is a view
*/
Q_DECL_CONSTEXPR int position() const
{
return positionInBuffer;
}
Q_DECL_CONSTEXPR bool isNull() const
{
return token.isNull();
}
Q_DECL_CONSTEXPR bool isEmpty() const
{
return token.isEmpty();
}
Q_DECL_CONSTEXPR QChar at(qsizetype n) const
{
return token.at(n);
}
};
/**
* @short AbstractTokenizer breaks text into smaller pieces - words, sentences, paragraphs.
*
* AbstractTokenizer is an abstract class that must be subclassed to be used. It provides API modelled
* after Java-style iterators. During tokenization buffer can be modified using provided replace() method.
*
* @since 4.3
*/
class AbstractTokenizer
{
public:
virtual ~AbstractTokenizer()
{
}
/**
* Sets text to tokenize. It also resets tokenizer state.
*/
virtual void setBuffer(const QString &buffer = QString()) = 0;
/**
* Returns true if there is another token available.
* @return true if another token is available, false if not.
*/
virtual bool hasNext() const = 0;
/**
* Returns next token or null QString if there is none
*/
virtual Token next() = 0;
/** Returns content of currently tokenized buffer*/
virtual QString buffer() const = 0;
/**
* Replace part of text in current buffer. Always use this function instead of directly
* changing data in underlying buffer or tokenizer's internal state may become inconsistent.
*/
virtual void replace(int position, int len, const QString &newWord) = 0;
};
class BreakTokenizerPrivate;
/**
@short WordTokenizer splits supplied buffer into individual words.
WordTokenizer splits buffer into words according to rules from Unicode standard 5.1.
If purpose is to check spelling, use isSpellcheckable() to determine if current word should be
checked or ignored.
Usage example:
@code
WordTokenizer t(buffer);
Speller sp;
while (t.hasNext()) {
Token word=t.next();
if (!t.isSpellcheckable()) continue;
qDebug() << word.toString() << " " << sp.isCorrect(word.toString());
}
@endcode
This example checks spelling of given buffer
* @since 4.3
*/
class SONNETCORE_EXPORT WordTokenizer : public AbstractTokenizer
{
public:
/**
* Constructor for word tokenizer
* @param buffer
*/
WordTokenizer(const QString &buffer = QString());
~WordTokenizer() override;
void setBuffer(const QString &buffer) override;
bool hasNext() const override;
Token next() override;
QString buffer() const override;
void replace(int position, int len, const QString &newWord) override;
/** Returns true if this word should be spell checked. This ignores email addresses, URLs and other things according to configuration */
bool isSpellcheckable() const;
/** If ignore uppercase is true, then any word containing only uppercase letters will be considered unsuitable for spell check */
void setIgnoreUppercase(bool val);
private:
SONNETCORE_NO_EXPORT bool isUppercase(QStringView word) const;
private:
std::unique_ptr<BreakTokenizerPrivate> const d;
};
/**
@short SentenceTokenizer splits supplied buffer into individual sentences.
SentenceTokenizer splits buffer into sentences according to rules from Unicode standard 5.1.
* @since 4.3
*/
class SONNETCORE_EXPORT SentenceTokenizer : public AbstractTokenizer
{
public:
SentenceTokenizer(const QString &buffer = QString());
~SentenceTokenizer() override;
void setBuffer(const QString &buffer) override;
bool hasNext() const override;
Token next() override;
QString buffer() const override;
void replace(int position, int len, const QString &newWord) override;
private:
std::unique_ptr<BreakTokenizerPrivate> const d;
};
}
#endif
@@ -0,0 +1,59 @@
# you can find find_package() in kdelibs/cmake/modules/OptionalFindPackage.cmake
# it is the same as FIND_PACKAGE(<name>) but additionally creates an OPTION(WITH_<name>)
# so the checking for the software can be disabled via ccmake or -DWITH_<name>=OFF
find_package(ASPELL)
set_package_properties(ASPELL PROPERTIES
URL "http://aspell.net/"
DESCRIPTION "Spell checking support via Aspell"
TYPE OPTIONAL
)
if (ASPELL_INCLUDE_DIR AND ASPELL_LIBRARIES)
add_subdirectory( aspell )
endif ()
find_package(HSPELL)
set_package_properties(HSPELL PROPERTIES
URL "http://ivrix.org.il/projects/spell-checker/"
DESCRIPTION "Spell checking support for Hebrew"
TYPE OPTIONAL
)
if (HSPELL_FOUND)
add_subdirectory( hspell )
endif ()
find_package(HUNSPELL)
set_package_properties(HUNSPELL PROPERTIES
URL "https://hunspell.github.io/"
DESCRIPTION "Spell checking support via Hunspell"
TYPE OPTIONAL
)
if (HUNSPELL_FOUND)
add_subdirectory( hunspell )
endif ()
find_package(VOIKKO)
set_package_properties(VOIKKO PROPERTIES
URL "https://voikko.puimula.org/"
DESCRIPTION "Spell checking support via Voikko"
TYPE OPTIONAL
)
if (VOIKKO_FOUND)
add_subdirectory( voikko )
endif ()
if (APPLE)
add_subdirectory( nsspellchecker )
endif ()
if (WIN32)
add_subdirectory( ispellchecker )
endif ()
# if we did not find any backend, that is bad
# do that on Android, too, if we have some backend for it
if (NOT ANDROID AND NOT SONNET_BACKEND_FOUND AND NOT SONNET_NO_BACKENDS)
message(FATAL_ERROR "Can not build any backend plugin for Sonnet.")
endif ()
@@ -0,0 +1,21 @@
add_library(sonnet_aspell MODULE
aspellclient.cpp
aspelldict.cpp
)
target_include_directories(sonnet_aspell PRIVATE ${ASPELL_INCLUDE_DIR})
ecm_qt_declare_logging_category(sonnet_aspell
HEADER aspell_debug.h
IDENTIFIER SONNET_LOG_ASPELL
CATEGORY_NAME kf.sonnet.clients.aspell
OLD_CATEGORY_NAMES sonnet.plugins.aspell
DESCRIPTION "Sonnet Aspell plugin"
EXPORT SONNET
)
target_link_libraries(sonnet_aspell PRIVATE KF6::SonnetCore ${ASPELL_LIBRARIES})
install(TARGETS sonnet_aspell DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf6/sonnet/)
set(SONNET_BACKEND_FOUND TRUE PARENT_SCOPE)
@@ -0,0 +1,56 @@
/*
* kspell_aspellclient.cpp
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "aspellclient.h"
#include "aspelldict.h"
#include "aspell_debug.h"
#ifdef Q_OS_WIN
#include <QCoreApplication>
#endif
using namespace Sonnet;
ASpellClient::ASpellClient(QObject *parent)
: Client(parent)
, m_config(new_aspell_config())
{
#ifdef Q_OS_WIN
aspell_config_replace(m_config, "data-dir", QString::fromLatin1("%1/data/aspell").arg(QCoreApplication::applicationDirPath()).toLatin1().constData());
aspell_config_replace(m_config, "dict-dir", QString::fromLatin1("%1/data/aspell").arg(QCoreApplication::applicationDirPath()).toLatin1().constData());
#endif
}
ASpellClient::~ASpellClient()
{
delete_aspell_config(m_config);
}
SpellerPlugin *ASpellClient::createSpeller(const QString &language)
{
ASpellDict *ad = new ASpellDict(language);
return ad;
}
QStringList ASpellClient::languages() const
{
AspellDictInfoList *l = get_aspell_dict_info_list(m_config);
AspellDictInfoEnumeration *el = aspell_dict_info_list_elements(l);
QStringList langs;
const AspellDictInfo *di = nullptr;
while ((di = aspell_dict_info_enumeration_next(el))) {
langs.append(QString::fromLatin1(di->name));
}
delete_aspell_dict_info_enumeration(el);
return langs;
}
#include "moc_aspellclient.cpp"
@@ -0,0 +1,49 @@
/*
* kspell_aspellclient.h
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_ASPELLCLIENT_H
#define KSPELL_ASPELLCLIENT_H
#include "client_p.h"
#include "aspell.h"
namespace Sonnet
{
class SpellerPlugin;
}
using Sonnet::SpellerPlugin;
class ASpellClient : public Sonnet::Client
{
Q_OBJECT
Q_INTERFACES(Sonnet::Client)
Q_PLUGIN_METADATA(IID "org.kde.Sonnet.ASpellClient")
public:
explicit ASpellClient(QObject *parent = nullptr);
~ASpellClient() override;
int reliability() const override
{
return 20;
}
SpellerPlugin *createSpeller(const QString &language) override;
QStringList languages() const override;
QString name() const override
{
return QStringLiteral("ASpell");
}
private:
AspellConfig *const m_config;
};
#endif
@@ -0,0 +1,116 @@
/*
* kspell_aspelldict.cpp
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "aspelldict.h"
#include "aspell_debug.h"
#ifdef Q_OS_WIN
#include <QCoreApplication>
#endif
using namespace Sonnet;
ASpellDict::ASpellDict(const QString &lang)
: SpellerPlugin(lang)
{
m_config = new_aspell_config();
aspell_config_replace(m_config, "lang", lang.toLatin1().constData());
/* All communication with Aspell is done in UTF-8 */
/* For reference, please look at BR#87250 */
aspell_config_replace(m_config, "encoding", "utf-8");
#ifdef Q_OS_WIN
aspell_config_replace(m_config, "data-dir", QString::fromLatin1("%1/data/aspell").arg(QCoreApplication::applicationDirPath()).toLatin1().constData());
aspell_config_replace(m_config, "dict-dir", QString::fromLatin1("%1/data/aspell").arg(QCoreApplication::applicationDirPath()).toLatin1().constData());
#endif
AspellCanHaveError *possible_err = new_aspell_speller(m_config);
if (aspell_error_number(possible_err) != 0) {
qCWarning(SONNET_LOG_ASPELL) << "aspell error: " << aspell_error_message(possible_err);
} else {
m_speller = to_aspell_speller(possible_err);
}
}
ASpellDict::~ASpellDict()
{
delete_aspell_speller(m_speller);
delete_aspell_config(m_config);
}
bool ASpellDict::isCorrect(const QString &word) const
{
/* ASpell is expecting length of a string in char representation */
/* word.length() != word.toUtf8().length() for nonlatin strings */
if (!m_speller) {
return false;
}
int correct = aspell_speller_check(m_speller, word.toUtf8().constData(), word.toUtf8().length());
return correct;
}
QStringList ASpellDict::suggest(const QString &word) const
{
if (!m_speller) {
return QStringList();
}
/* ASpell is expecting length of a string in char representation */
/* word.length() != word.toUtf8().length() for nonlatin strings */
const AspellWordList *suggestions = aspell_speller_suggest(m_speller, word.toUtf8().constData(), word.toUtf8().length());
AspellStringEnumeration *elements = aspell_word_list_elements(suggestions);
QStringList qsug;
const char *cword;
while ((cword = aspell_string_enumeration_next(elements))) {
/* Since while creating the class ASpellDict the encoding is set */
/* to utf-8, one has to convert output from Aspell to QString's UTF-16 */
qsug.append(QString::fromUtf8(cword));
}
delete_aspell_string_enumeration(elements);
return qsug;
}
bool ASpellDict::storeReplacement(const QString &bad, const QString &good)
{
if (!m_speller) {
return false;
}
/* ASpell is expecting length of a string in char representation */
/* word.length() != word.toUtf8().length() for nonlatin strings */
return aspell_speller_store_replacement(m_speller, bad.toUtf8().constData(), bad.toUtf8().length(), good.toUtf8().constData(), good.toUtf8().length());
}
bool ASpellDict::addToPersonal(const QString &word)
{
if (!m_speller) {
return false;
}
qCDebug(SONNET_LOG_ASPELL) << "Adding" << word << "to aspell personal dictionary";
/* ASpell is expecting length of a string in char representation */
/* word.length() != word.toUtf8().length() for nonlatin strings */
aspell_speller_add_to_personal(m_speller, word.toUtf8().constData(), word.toUtf8().length());
/* Add is not enough, one has to save it. This is not documented */
/* in ASpell's API manual. I found it in */
/* aspell-0.60.2/example/example-c.c */
return aspell_speller_save_all_word_lists(m_speller);
}
bool ASpellDict::addToSession(const QString &word)
{
if (!m_speller) {
return false;
}
/* ASpell is expecting length of a string in char representation */
/* word.length() != word.toUtf8().length() for nonlatin strings */
return aspell_speller_add_to_session(m_speller, word.toUtf8().constData(), word.toUtf8().length());
}
@@ -0,0 +1,34 @@
/*
* kspell_aspelldict.h
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_ASPELLDICT_H
#define KSPELL_ASPELLDICT_H
#include "spellerplugin_p.h"
#include "aspell.h"
class ASpellDict : public Sonnet::SpellerPlugin
{
public:
explicit ASpellDict(const QString &lang);
~ASpellDict() override;
bool isCorrect(const QString &word) const override;
QStringList suggest(const QString &word) const override;
bool storeReplacement(const QString &bad, const QString &good) override;
bool addToPersonal(const QString &word) override;
bool addToSession(const QString &word) override;
private:
AspellConfig *m_config = nullptr;
AspellSpeller *m_speller = nullptr;
};
#endif
@@ -0,0 +1,30 @@
find_package(ZLIB)
set_package_properties(ZLIB PROPERTIES DESCRIPTION "Support for gzip compressed files and data streams"
URL "https://www.zlib.net"
TYPE REQUIRED
PURPOSE "Required by the hspell sonnet plugin"
)
add_library(sonnet_hspell MODULE
hspellclient.cpp
hspelldict.cpp
)
target_include_directories(sonnet_hspell PRIVATE
${HSPELL_INCLUDE_DIR}
)
ecm_qt_declare_logging_category(sonnet_hspell
HEADER hspell_debug.h
IDENTIFIER SONNET_LOG_HSPELL
CATEGORY_NAME kf.sonnet.clients.hspell
OLD_CATEGORY_NAMES sonnet.plugins.hspell
DESCRIPTION "Sonnet Hspell plugin"
EXPORT SONNET
)
target_link_libraries(sonnet_hspell PRIVATE KF6::SonnetCore ${HSPELL_LIBRARIES} ZLIB::ZLIB)
install(TARGETS sonnet_hspell DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf6/sonnet/)
set(SONNET_BACKEND_FOUND TRUE PARENT_SCOPE)
@@ -0,0 +1,43 @@
/*
* kspell_hspellclient.cpp
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2005 Mashrab Kuvatov <kmashrab@uni-bremen.de>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "hspellclient.h"
#include "hspell.h"
#include "hspelldict.h"
#include <QFileInfo>
#include <QUrl>
using namespace Sonnet;
HSpellClient::HSpellClient(QObject *parent)
: Client(parent)
{
}
HSpellClient::~HSpellClient()
{
}
SpellerPlugin *HSpellClient::createSpeller(const QString &language)
{
HSpellDict *ad = new HSpellDict(language);
return ad;
}
QStringList HSpellClient::languages() const
{
QString dictPath(QString::fromUtf8(hspell_get_dictionary_path()));
if (QUrl(dictPath).isLocalFile() && QFileInfo::exists(dictPath)) {
return {QStringLiteral("he")};
}
return {};
}
#include "moc_hspellclient.cpp"
@@ -0,0 +1,51 @@
/*
* kspell_hspellclient.h
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2005 Mashrab Kuvatov <kmashrab@uni-bremen.de>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_HSPELLCLIENT_H
#define KSPELL_HSPELLCLIENT_H
#include "client_p.h"
/* libhspell is a C library and it does not have #ifdef __cplusplus */
extern "C" {
#include "hspell.h"
}
namespace Sonnet
{
class SpellerPlugin;
}
using Sonnet::SpellerPlugin;
class HSpellClient : public Sonnet::Client
{
Q_OBJECT
Q_INTERFACES(Sonnet::Client)
Q_PLUGIN_METADATA(IID "org.kde.Sonnet.HSpellClient")
public:
explicit HSpellClient(QObject *parent = nullptr);
~HSpellClient();
int reliability() const override
{
return 20;
}
SpellerPlugin *createSpeller(const QString &language) override;
QStringList languages() const override;
QString name() const override
{
return QString::fromLatin1("HSpell");
}
private:
};
#endif
@@ -0,0 +1,131 @@
/*
* kspell_hspelldict.cpp
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2005 Mashrab Kuvatov <kmashrab@uni-bremen.de>
* SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "hspelldict.h"
#include "hspell_debug.h"
#include <QSettings>
using namespace Sonnet;
HSpellDict::HSpellDict(const QString &lang)
: SpellerPlugin(lang)
{
int int_error = hspell_init(&m_speller, HSPELL_OPT_DEFAULT);
if (int_error == -1) {
qCWarning(SONNET_LOG_HSPELL) << "HSpellDict::HSpellDict: Init failed";
initialized = false;
} else {
/* hspell understands only iso8859-8-i */
m_decoder = QStringDecoder("iso8859-8-i");
m_encoder = QStringEncoder("iso8859-8-i");
initialized = true;
}
QSettings settings(QStringLiteral("KDE"), QStringLiteral("SonnetHSpellPlugin"));
const QStringList personalWordsList = settings.value(QStringLiteral("PersonalWords"), QStringList()).toStringList();
m_personalWords = QSet<QString>(personalWordsList.begin(), personalWordsList.end());
QVariantHash replacementMap = settings.value(QStringLiteral("Replacements"), QVariant()).toHash();
for (const QString &key : replacementMap.keys()) {
m_replacements[key] = replacementMap[key].toString();
}
}
HSpellDict::~HSpellDict()
{
/* It exists in =< hspell-0.8 */
if (initialized) {
hspell_uninit(m_speller);
}
}
bool HSpellDict::isCorrect(const QString &word) const
{
if (m_sessionWords.contains(word)) {
return true;
}
if (m_personalWords.contains(word)) {
return true;
}
if (!initialized) {
// Not much we can do, so just return true (less annoying for the user)
return true;
}
int preflen;
QByteArray wordISO = m_encoder.encode(word);
// returns 1 if the word is correct, 0 otherwise
int correct = hspell_check_word(m_speller, wordISO.constData(),
&preflen); // this argument might be removed, it isn't useful
// gimatria is a representation of numbers with hebrew letters, we accept these
if (correct != 1) {
if (hspell_is_canonic_gimatria(wordISO.constData()) != 0) {
correct = 1;
}
}
return correct == 1;
}
QStringList HSpellDict::suggest(const QString &word) const
{
QStringList suggestions;
if (m_replacements.contains(word)) {
suggestions.append(m_replacements[word]);
}
struct corlist correctionList;
int suggestionCount;
corlist_init(&correctionList);
const QByteArray encodedWord = m_encoder.encode(word);
hspell_trycorrect(m_speller, encodedWord.constData(), &correctionList);
for (suggestionCount = 0; suggestionCount < corlist_n(&correctionList); suggestionCount++) {
suggestions.append(m_decoder.decode(corlist_str(&correctionList, suggestionCount)));
}
corlist_free(&correctionList);
return suggestions;
}
bool HSpellDict::storeReplacement(const QString &bad, const QString &good)
{
m_replacements[bad] = good;
storePersonalWords();
return true;
}
bool HSpellDict::addToPersonal(const QString &word)
{
m_personalWords.insert(word);
storePersonalWords();
return true;
}
bool HSpellDict::addToSession(const QString &word)
{
m_sessionWords.insert(word);
return true;
}
void HSpellDict::storePersonalWords()
{
QSettings settings(QStringLiteral("KDE"), QStringLiteral("SonnetHSpellPlugin"));
const QStringList personalWordsList(m_personalWords.begin(), m_personalWords.end());
settings.setValue(QStringLiteral("PersonalWords"), QVariant(personalWordsList));
QVariantHash variantHash;
for (const QString &key : m_replacements.keys()) {
variantHash[key] = QVariant(m_replacements[key]);
}
settings.setValue(QStringLiteral("Replacements"), variantHash);
}
@@ -0,0 +1,52 @@
/*
* kspell_hspelldict.h
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2005 Mashrab Kuvatov <kmashrab@uni-bremen.de>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_HSPELLDICT_H
#define KSPELL_HSPELLDICT_H
#include <QSet>
#include <QStringDecoder>
#include <QStringEncoder>
#include "spellerplugin_p.h"
/* libhspell is a C library and it does not have #ifdef __cplusplus */
extern "C" {
#include "hspell.h"
}
class HSpellDict : public Sonnet::SpellerPlugin
{
public:
explicit HSpellDict(const QString &lang);
~HSpellDict();
bool isCorrect(const QString &word) const override;
QStringList suggest(const QString &word) const override;
bool storeReplacement(const QString &bad, const QString &good) override;
bool addToPersonal(const QString &word) override;
bool addToSession(const QString &word) override;
inline bool isInitialized() const
{
return initialized;
}
private:
void storePersonalWords();
struct dict_radix *m_speller;
mutable QStringDecoder m_decoder;
mutable QStringEncoder m_encoder;
bool initialized;
QSet<QString> m_sessionWords;
QSet<QString> m_personalWords;
QHash<QString, QString> m_replacements;
};
#endif
@@ -0,0 +1,31 @@
add_library(sonnet_hunspell MODULE
hunspellclient.cpp
hunspelldict.cpp
)
ecm_qt_declare_logging_category(sonnet_hunspell
HEADER hunspelldebug.h
IDENTIFIER SONNET_HUNSPELL
CATEGORY_NAME kf.sonnet.clients.hunspell
OLD_CATEGORY_NAMES sonnet.plugins.hunspell
DESCRIPTION "Sonnet HUnspell plugin"
EXPORT SONNET
)
# see: https://phabricator.kde.org/R246:0a96acf251baa5c9dd042d093ab2bf8fcee10502
set(USE_OLD_HUNSPELL_API TRUE)
if (PKG_HUNSPELL_VERSION GREATER "1.5.0")
set(USE_OLD_HUNSPELL_API FALSE) # new API introduced in v1.5.1 (cf. https://github.com/hunspell/hunspell/commit/8006703dafeebce19f2144c5cf180812eb99693a)
endif()
message(STATUS "Using old hunspell API: ${USE_OLD_HUNSPELL_API}")
configure_file(config-hunspell.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-hunspell.h)
target_include_directories(sonnet_hunspell SYSTEM PUBLIC ${HUNSPELL_INCLUDE_DIRS})
target_link_libraries(sonnet_hunspell PRIVATE KF6::SonnetCore ${HUNSPELL_LIBRARIES})
target_compile_definitions(sonnet_hunspell PRIVATE DEFINITIONS SONNET_INSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}")
install(TARGETS sonnet_hunspell DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf6/sonnet/)
set(SONNET_BACKEND_FOUND TRUE PARENT_SCOPE)
@@ -0,0 +1 @@
#cmakedefine01 USE_OLD_HUNSPELL_API
@@ -0,0 +1,19 @@
TARGET = sonnet-hunspell
TEMPLATE = lib
CONFIG += staticlib
QT -= gui
SOURCES += hunspelldict.cpp \
hunspellclient.cpp \
hunspelldebug.cpp
HEADERS += hunspellclient.h
DEFINES += SONNETUI_EXPORT=""
DEFINES += SONNETCORE_EXPORT=""
DEFINES += INSTALLATION_PLUGIN_PATH=""
DEFINES += SONNET_STATIC
INCLUDEPATH += ../../core
INCLUDEPATH += ../../../../hunspell/src
@@ -0,0 +1,91 @@
/*
* kspell_hunspellclient.cpp
*
* SPDX-FileCopyrightText: 2009 Montel Laurent <montel@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "hunspellclient.h"
#include "hunspelldebug.h"
#include "hunspelldict.h"
#include <QDir>
#include <QStandardPaths>
#include <QString>
using namespace Sonnet;
HunspellClient::HunspellClient(QObject *parent)
: Client(parent)
{
qCDebug(SONNET_HUNSPELL) << " HunspellClient::HunspellClient";
QStringList dirList;
// search QStandardPaths
dirList.append(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("hunspell"), QStandardPaths::LocateDirectory));
auto maybeAddPath = [&dirList](const QString &path) {
if (QFileInfo::exists(path)) {
dirList.append(path);
QDir dir(path);
for (const QString &subDir : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
dirList.append(dir.absoluteFilePath(subDir));
}
}
};
#ifdef Q_OS_WIN
maybeAddPath(QStringLiteral(SONNET_INSTALL_PREFIX "/bin/data/hunspell/"));
#else
maybeAddPath(QStringLiteral("/System/Library/Spelling"));
maybeAddPath(QStringLiteral("/usr/share/hunspell/"));
maybeAddPath(QStringLiteral("/usr/share/myspell/"));
#endif
for (const QString &dirString : dirList) {
QDir dir(dirString);
const QList<QFileInfo> dicts = dir.entryInfoList({QStringLiteral("*.aff")}, QDir::Files);
for (const QFileInfo &dict : dicts) {
const QString language = dict.baseName();
if (dict.isSymbolicLink()) {
const QFileInfo actualDict(dict.canonicalFilePath());
const QString alias = actualDict.baseName();
if (language != alias) {
qCDebug(SONNET_HUNSPELL) << "Found alias" << language << "->" << alias;
m_languageAliases.insert(language, alias);
continue;
}
} else {
m_languagePaths.insert(language, dict.canonicalPath());
}
}
}
}
HunspellClient::~HunspellClient()
{
}
SpellerPlugin *HunspellClient::createSpeller(const QString &inputLang)
{
QString language = inputLang;
if (m_languageAliases.contains(language)) {
qCDebug(SONNET_HUNSPELL) << "Using alias" << m_languageAliases.value(language) << "for" << language;
language = m_languageAliases.value(language);
}
std::shared_ptr<Hunspell> hunspell = m_hunspellCache.value(language).lock();
if (!hunspell) {
hunspell = HunspellDict::createHunspell(language, m_languagePaths.value(language));
m_hunspellCache.insert(language, hunspell);
}
qCDebug(SONNET_HUNSPELL) << " SpellerPlugin *HunspellClient::createSpeller(const QString &language) ;" << language;
HunspellDict *ad = new HunspellDict(inputLang, hunspell);
return ad;
}
QStringList HunspellClient::languages() const
{
return m_languagePaths.keys() + m_languageAliases.keys();
}
#include "moc_hunspellclient.cpp"
@@ -0,0 +1,53 @@
/*
* kspell_hunspellclient.h
*
* SPDX-FileCopyrightText: 2009 Montel Laurent <montel@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_HUNSPELLCLIENT_H
#define KSPELL_HUNSPELLCLIENT_H
#include "client_p.h"
#include <QMap>
#include <memory>
class Hunspell;
namespace Sonnet
{
class SpellerPlugin;
}
using Sonnet::SpellerPlugin;
class HunspellClient : public Sonnet::Client
{
Q_OBJECT
Q_INTERFACES(Sonnet::Client)
Q_PLUGIN_METADATA(IID "org.kde.Sonnet.HunspellClient")
public:
explicit HunspellClient(QObject *parent = nullptr);
~HunspellClient() override;
int reliability() const override
{
return 40;
}
SpellerPlugin *createSpeller(const QString &language) override;
QStringList languages() const override;
QString name() const override
{
return QStringLiteral("Hunspell");
}
private:
QMap<QString, QString> m_languagePaths;
QMap<QString, std::weak_ptr<Hunspell>> m_hunspellCache;
QMap<QString, QString> m_languageAliases;
};
#endif
@@ -0,0 +1,180 @@
/*
* kspell_hunspelldict.cpp
*
* SPDX-FileCopyrightText: 2009 Montel Laurent <montel@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "hunspelldict.h"
#include "config-hunspell.h"
#include "hunspelldebug.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTextStream>
using namespace Sonnet;
HunspellDict::HunspellDict(const QString &lang, const std::shared_ptr<Hunspell> &speller)
: SpellerPlugin(lang)
{
if (!speller) {
qCWarning(SONNET_HUNSPELL) << "Can't create a client without a speller";
return;
}
m_decoder = QStringDecoder(speller->get_dic_encoding());
if (!m_decoder.isValid()) {
qCWarning(SONNET_HUNSPELL) << "Failed to find a text codec for name" << speller->get_dic_encoding() << "defaulting to locale text codec";
m_decoder = QStringDecoder(QStringDecoder::System);
Q_ASSERT(m_decoder.isValid());
}
m_encoder = QStringEncoder(speller->get_dic_encoding());
if (!m_encoder.isValid()) {
qCWarning(SONNET_HUNSPELL) << "Failed to find a text codec for name" << speller->get_dic_encoding() << "defaulting to locale text codec";
m_encoder = QStringEncoder(QStringEncoder::System);
Q_ASSERT(m_encoder.isValid());
}
m_speller = speller;
const QString userDic = QDir::home().filePath(QLatin1String(".hunspell_") % lang);
QFile userDicFile(userDic);
if (userDicFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qCDebug(SONNET_HUNSPELL) << "Load a user dictionary" << userDic;
QTextStream userDicIn(&userDicFile);
while (!userDicIn.atEnd()) {
const QString word = userDicIn.readLine();
if (word.isEmpty()) {
continue;
}
if (word.contains(QLatin1Char('/'))) {
QStringList wordParts = word.split(QLatin1Char('/'));
speller->add_with_affix(toDictEncoding(wordParts.at(0)).constData(), toDictEncoding(wordParts.at(1)).constData());
}
if (word.at(0) == QLatin1Char('*')) {
speller->remove(toDictEncoding(word.mid(1)).constData());
} else {
speller->add(toDictEncoding(word).constData());
}
}
userDicFile.close();
}
}
std::shared_ptr<Hunspell> HunspellDict::createHunspell(const QString &lang, QString path)
{
qCDebug(SONNET_HUNSPELL) << "Loading dictionary for" << lang << "from" << path;
if (!path.endsWith(QLatin1Char('/'))) {
path += QLatin1Char('/');
}
path += lang;
QString dictionary = path + QStringLiteral(".dic");
QString aff = path + QStringLiteral(".aff");
if (!QFileInfo::exists(dictionary) || !QFileInfo::exists(aff)) {
qCWarning(SONNET_HUNSPELL) << "Unable to find dictionary for" << lang << "in path" << path;
return nullptr;
}
std::shared_ptr<Hunspell> speller = std::make_shared<Hunspell>(aff.toLocal8Bit().constData(), dictionary.toLocal8Bit().constData());
qCDebug(SONNET_HUNSPELL) << "Created " << speller.get();
return speller;
}
HunspellDict::~HunspellDict()
{
}
QByteArray HunspellDict::toDictEncoding(const QString &word) const
{
if (m_encoder.isValid()) {
return m_encoder.encode(word);
}
return {};
}
bool HunspellDict::isCorrect(const QString &word) const
{
qCDebug(SONNET_HUNSPELL) << " isCorrect :" << word;
if (!m_speller) {
return false;
}
#if USE_OLD_HUNSPELL_API
int result = m_speller->spell(toDictEncoding(word).constData());
qCDebug(SONNET_HUNSPELL) << " result :" << result;
return result != 0;
#else
bool result = m_speller->spell(toDictEncoding(word).toStdString());
qCDebug(SONNET_HUNSPELL) << " result :" << result;
return result;
#endif
}
QStringList HunspellDict::suggest(const QString &word) const
{
if (!m_speller) {
return QStringList();
}
QStringList lst;
#if USE_OLD_HUNSPELL_API
char **selection;
int nbWord = m_speller->suggest(&selection, toDictEncoding(word).constData());
for (int i = 0; i < nbWord; ++i) {
lst << m_decoder.decode(selection[i]);
}
m_speller->free_list(&selection, nbWord);
#else
const auto suggestions = m_speller->suggest(toDictEncoding(word).toStdString());
for_each(suggestions.begin(), suggestions.end(), [this, &lst](const std::string &suggestion) {
lst << m_decoder.decode(suggestion.c_str());
});
#endif
return lst;
}
bool HunspellDict::storeReplacement(const QString &bad, const QString &good)
{
Q_UNUSED(bad);
Q_UNUSED(good);
if (!m_speller) {
return false;
}
qCDebug(SONNET_HUNSPELL) << "HunspellDict::storeReplacement not implemented";
return false;
}
bool HunspellDict::addToPersonal(const QString &word)
{
if (!m_speller) {
return false;
}
m_speller->add(toDictEncoding(word).constData());
const QString userDic = QDir::home().filePath(QLatin1String(".hunspell_") % language());
QFile userDicFile(userDic);
if (userDicFile.open(QIODevice::Append | QIODevice::Text)) {
QTextStream out(&userDicFile);
out << word << '\n';
userDicFile.close();
return true;
}
return false;
}
bool HunspellDict::addToSession(const QString &word)
{
if (!m_speller) {
return false;
}
int r = m_speller->add(toDictEncoding(word).constData());
return r == 0;
}
@@ -0,0 +1,43 @@
/*
* kspell_aspelldict.h
*
* SPDX-FileCopyrightText: 2009 Montel Laurent <montel@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_HUNSPELLDICT_H
#define KSPELL_HUNSPELLDICT_H
#include "hunspell.hxx"
#include "spellerplugin_p.h"
#include <QStringDecoder>
#include <QStringEncoder>
#include <memory>
class HunspellDict : public Sonnet::SpellerPlugin
{
public:
explicit HunspellDict(const QString &name, const std::shared_ptr<Hunspell> &speller);
~HunspellDict() override;
bool isCorrect(const QString &word) const override;
QStringList suggest(const QString &word) const override;
bool storeReplacement(const QString &bad, const QString &good) override;
bool addToPersonal(const QString &word) override;
bool addToSession(const QString &word) override;
static std::shared_ptr<Hunspell> createHunspell(const QString &lang, QString path);
private:
QByteArray toDictEncoding(const QString &word) const;
std::shared_ptr<Hunspell> m_speller;
mutable QStringEncoder m_encoder;
mutable QStringDecoder m_decoder;
};
#endif
@@ -0,0 +1,31 @@
include(CheckIncludeFile)
# needs windows 8 or higher
add_definitions(-DWINVER=0x0602 -D_WIN32_WINNT=0x0602)
CHECK_INCLUDE_FILE(spellcheck.h HAS_SPELLCHECK_H)
if (NOT HAS_SPELLCHECK_H)
return()
endif()
add_library(sonnet_ispellchecker MODULE
ispellcheckerclient.cpp
ispellcheckerdict.cpp
)
ecm_qt_declare_logging_category(sonnet_ispellchecker
HEADER ispellcheckerdebug.h
IDENTIFIER SONNET_ISPELLCHECKER
CATEGORY_NAME kf.sonnet.clients.ispellchecker
OLD_CATEGORY_NAMES sonnet.plugins.ispellchecker
DESCRIPTION "Sonnet ISpellChecker plugin"
EXPORT SONNET
)
target_link_libraries(sonnet_ispellchecker PRIVATE KF6::SonnetCore)
target_compile_definitions(sonnet_ispellchecker PRIVATE DEFINITIONS SONNET_INSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}")
install(TARGETS sonnet_ispellchecker DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf6/sonnet/)
set(SONNET_BACKEND_FOUND TRUE PARENT_SCOPE)
@@ -0,0 +1,66 @@
/*
SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "ispellcheckerclient.h"
#include "ispellcheckerdebug.h"
#include "ispellcheckerdict.h"
using namespace Sonnet;
ISpellCheckerClient::ISpellCheckerClient(QObject *parent)
: Client(parent)
{
qCDebug(SONNET_ISPELLCHECKER) << " ISpellCheckerClient::ISpellCheckerClient";
// init com if needed, use same variant as e.g. Qt in qtbase/src/corelib/io/qfilesystemengine_win.cpp
CoInitialize(nullptr);
// get factory & collect all known languages + instantiate the spell checkers for them
ISpellCheckerFactory *spellCheckerFactory = nullptr;
if (SUCCEEDED(CoCreateInstance(__uuidof(SpellCheckerFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&spellCheckerFactory))) && spellCheckerFactory) {
// if we have a factory, cache the language names
IEnumString *enumLanguages = nullptr;
if (SUCCEEDED(spellCheckerFactory->get_SupportedLanguages(&enumLanguages))) {
HRESULT hr = S_OK;
while (S_OK == hr) {
LPOLESTR string = nullptr;
hr = enumLanguages->Next(1, &string, nullptr);
if (S_OK == hr) {
ISpellChecker *spellChecker = nullptr;
if (SUCCEEDED(spellCheckerFactory->CreateSpellChecker(string, &spellChecker)) && spellChecker) {
m_languages.insert(QString::fromWCharArray(string), spellChecker);
}
CoTaskMemFree(string);
}
}
enumLanguages->Release();
}
spellCheckerFactory->Release();
}
}
ISpellCheckerClient::~ISpellCheckerClient()
{
// FIXME: we at the moment leak all checkers as sonnet does the cleanup to late for proper com cleanup :/
}
SpellerPlugin *ISpellCheckerClient::createSpeller(const QString &language)
{
// create requested spellchecker if we know the language
qCDebug(SONNET_ISPELLCHECKER) << " SpellerPlugin *ISpellCheckerClient::createSpeller(const QString &language) ;" << language;
const auto it = m_languages.find(language);
if (it != m_languages.end()) {
return new ISpellCheckerDict(it.value(), language);
}
return nullptr;
}
QStringList ISpellCheckerClient::languages() const
{
return m_languages.keys();
}
#include "moc_ispellcheckerclient.cpp"
@@ -0,0 +1,51 @@
/*
SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KSPELL_ISPELLCHECKCLIENT_H
#define KSPELL_ISPELLCHECKCLIENT_H
#include "client_p.h"
#include <spellcheck.h>
#include <windows.h>
#include <QMap>
namespace Sonnet
{
class SpellerPlugin;
}
using Sonnet::SpellerPlugin;
class ISpellCheckerClient : public Sonnet::Client
{
Q_OBJECT
Q_INTERFACES(Sonnet::Client)
Q_PLUGIN_METADATA(IID "org.kde.Sonnet.ISpellCheckerClient")
public:
explicit ISpellCheckerClient(QObject *parent = nullptr);
~ISpellCheckerClient() override;
int reliability() const override
{
return 40;
}
SpellerPlugin *createSpeller(const QString &language) override;
QStringList languages() const override;
QString name() const override
{
return QStringLiteral("ISpellChecker");
}
private:
// we internally keep all spell checker interfaces alive
QMap<QString, ISpellChecker *> m_languages;
};
#endif
@@ -0,0 +1,78 @@
/*
SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "ispellcheckerdict.h"
#include "ispellcheckerdebug.h"
using namespace Sonnet;
ISpellCheckerDict::ISpellCheckerDict(ISpellChecker *spellChecker, const QString &language)
: SpellerPlugin(language)
, m_spellChecker(spellChecker)
{
Q_ASSERT(m_spellChecker);
}
ISpellCheckerDict::~ISpellCheckerDict()
{
// we don't own m_spellChecker!
}
bool ISpellCheckerDict::isCorrect(const QString &word) const
{
// check if we are incorrect, we only need to check one enum entry for that, only empty enum means OK
bool ok = true;
IEnumSpellingError *enumSpellingError = nullptr;
if (SUCCEEDED(m_spellChecker->Check(word.toStdWString().c_str(), &enumSpellingError))) {
ISpellingError *spellingError = nullptr;
if (S_OK == enumSpellingError->Next(&spellingError)) {
ok = false;
spellingError->Release();
}
enumSpellingError->Release();
}
return ok;
}
QStringList ISpellCheckerDict::suggest(const QString &word) const
{
// query suggestions
QStringList replacements;
IEnumString *words = nullptr;
if (SUCCEEDED(m_spellChecker->Suggest(word.toStdWString().c_str(), &words))) {
HRESULT hr = S_OK;
while (S_OK == hr) {
LPOLESTR string = nullptr;
hr = words->Next(1, &string, nullptr);
if (S_OK == hr) {
replacements.push_back(QString::fromWCharArray(string));
CoTaskMemFree(string);
}
}
words->Release();
}
return replacements;
}
bool ISpellCheckerDict::storeReplacement(const QString &bad, const QString &good)
{
Q_UNUSED(bad);
Q_UNUSED(good);
qCDebug(SONNET_ISPELLCHECKER) << "ISpellCheckerDict::storeReplacement not implemented";
return false;
}
bool ISpellCheckerDict::addToPersonal(const QString &word)
{
// add word "permanently" to the dictionary
return SUCCEEDED(m_spellChecker->Add(word.toStdWString().c_str()));
}
bool ISpellCheckerDict::addToSession(const QString &word)
{
// ignore word for this session
return SUCCEEDED(m_spellChecker->Ignore(word.toStdWString().c_str()));
}
@@ -0,0 +1,33 @@
/*
SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KSPELL_ISPELLCHECKDICT_H
#define KSPELL_ISPELLCHECKDICT_H
#include "spellerplugin_p.h"
#include "ispellcheckerclient.h"
class ISpellCheckerDict : public Sonnet::SpellerPlugin
{
public:
explicit ISpellCheckerDict(ISpellChecker *spellChecker, const QString &language);
~ISpellCheckerDict() override;
bool isCorrect(const QString &word) const override;
QStringList suggest(const QString &word) const override;
bool storeReplacement(const QString &bad, const QString &good) override;
bool addToPersonal(const QString &word) override;
bool addToSession(const QString &word) override;
private:
// spell checker com object, we don't own this
ISpellChecker *const m_spellChecker;
};
#endif
@@ -0,0 +1,19 @@
add_library(sonnet_nsspellchecker MODULE
nsspellcheckerclient.mm
nsspellcheckerdict.mm
)
ecm_qt_declare_logging_category(sonnet_nsspellchecker
HEADER nsspellcheckerdebug.h
IDENTIFIER SONNET_NSSPELLCHECKER
CATEGORY_NAME kf.sonnet.clients.nsspellchecker
OLD_CATEGORY_NAMES sonnet.plugins.nsspellchecker
DESCRIPTION "Sonnet NSSpellChecker plugin"
EXPORT SONNET
)
target_link_libraries(sonnet_nsspellchecker PRIVATE KF6::SonnetCore "-framework AppKit")
install(TARGETS sonnet_nsspellchecker DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf6/sonnet/)
set(SONNET_BACKEND_FOUND TRUE PARENT_SCOPE)
@@ -0,0 +1,17 @@
TARGET = sonnet-nsspellchecker
TEMPLATE = lib
CONFIG += staticlib
QT -= gui
OBJECTIVE_SOURCES += nsspellcheckerdict.mm \
nsspellcheckerclient.mm
HEADERS += nsspellcheckerclient.h
DEFINES += SONNETUI_EXPORT=""
DEFINES += SONNETCORE_EXPORT=""
DEFINES += INSTALLATION_PLUGIN_PATH=""
DEFINES += SONNET_STATIC
INCLUDEPATH += ../../core
@@ -0,0 +1,38 @@
/*
* nsspellcheckerclient.h
*
* SPDX-FileCopyrightText: 2015 Nick Shaforostoff <shaforostoff@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_NSSPELLCLIENT_H
#define KSPELL_NSSPELLCLIENT_H
#include "client_p.h"
namespace Sonnet
{
class SpellerPlugin;
}
using Sonnet::SpellerPlugin;
class NSSpellCheckerClient : public Sonnet::Client
{
Q_OBJECT
Q_INTERFACES(Sonnet::Client)
Q_PLUGIN_METADATA(IID "org.kde.Sonnet.NSSpellClient")
public:
explicit NSSpellCheckerClient(QObject *parent = nullptr);
~NSSpellCheckerClient();
int reliability() const;
SpellerPlugin *createSpeller(const QString &language);
QStringList languages() const;
QString name() const
{
return QStringLiteral("NSSpellChecker");
}
};
#endif
@@ -0,0 +1,46 @@
/*
* nsspellcheckerclient.mm
*
* SPDX-FileCopyrightText: 2015 Nick Shaforostoff <shaforostoff@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "nsspellcheckerclient.h"
#include "nsspellcheckerdict.h"
#import <AppKit/AppKit.h>
using namespace Sonnet;
NSSpellCheckerClient::NSSpellCheckerClient(QObject *parent)
: Client(parent)
{
}
NSSpellCheckerClient::~NSSpellCheckerClient()
{
}
int NSSpellCheckerClient::reliability() const
{
return qEnvironmentVariableIsSet("SONNET_PREFER_NSSPELLCHECKER") ? 9999 : 30;
}
SpellerPlugin *NSSpellCheckerClient::createSpeller(const QString &language)
{
return new NSSpellCheckerDict(language);
}
QStringList NSSpellCheckerClient::languages() const
{
QStringList lst;
NSArray* availableLanguages = [[NSSpellChecker sharedSpellChecker]
availableLanguages];
for (NSString* lang_code in availableLanguages) {
lst.append(QString::fromNSString(lang_code));
}
return lst;
}
#include "moc_nsspellcheckerclient.cpp"
@@ -0,0 +1,35 @@
/*
* nsspellcheckerdict.h
*
* SPDX-FileCopyrightText: 2015 Nick Shaforostoff <shaforostoff@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KSPELL_NSSPELLDICT_H
#define KSPELL_NSSPELLDICT_H
#include "spellerplugin_p.h"
class NSSpellCheckerDict : public Sonnet::SpellerPlugin
{
public:
explicit NSSpellCheckerDict(const QString &lang);
~NSSpellCheckerDict();
virtual bool isCorrect(const QString &word) const;
virtual QStringList suggest(const QString &word) const;
virtual bool storeReplacement(const QString &bad, const QString &good);
virtual bool addToPersonal(const QString &word);
virtual bool addToSession(const QString &word);
private:
#ifdef __OBJC__
NSString *m_langCode;
#else
void *m_langCode;
#endif
};
#endif
@@ -0,0 +1,100 @@
/*
* nsspellcheckerdict.mm
*
* SPDX-FileCopyrightText: 2015 Nick Shaforostoff <shaforostoff@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "nsspellcheckerdict.h"
#include "nsspellcheckerdebug.h"
#import <AppKit/AppKit.h>
using namespace Sonnet;
NSSpellCheckerDict::NSSpellCheckerDict(const QString &lang)
: SpellerPlugin(lang)
, m_langCode([lang.toNSString() retain])
{
NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker];
if ([checker setLanguage:m_langCode]) {
qCDebug(SONNET_NSSPELLCHECKER) << "Loading dictionary for" << lang;
[checker updatePanels];
} else {
qCWarning(SONNET_NSSPELLCHECKER) << "Loading dictionary for unsupported language" << lang;
}
}
NSSpellCheckerDict::~NSSpellCheckerDict()
{
[m_langCode release];
}
bool NSSpellCheckerDict::isCorrect(const QString &word) const
{
NSString *nsWord = word.toNSString();
NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker];
NSRange range = [checker checkSpellingOfString:nsWord
startingAt:0 language:m_langCode
wrap:NO inSpellDocumentWithTag:0 wordCount:nullptr];
if (range.length == 0) {
// Check if the user configured a replacement text for this string. Sadly
// we can only signal an error if that's the case, Sonnet has no other way
// to take such substitutions into account.
if (NSDictionary *replacements = [checker userReplacementsDictionary]) {
return [replacements objectForKey:nsWord] == nil;
} else {
return true;
}
}
return false;
}
QStringList NSSpellCheckerDict::suggest(const QString &word) const
{
NSString *nsWord = word.toNSString();
NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker];
NSArray *suggestions = [checker guessesForWordRange:NSMakeRange(0, word.length())
inString:nsWord language:m_langCode inSpellDocumentWithTag:0];
QStringList lst;
NSDictionary *replacements = [checker userReplacementsDictionary];
QString replacement;
if ([replacements objectForKey:nsWord]) {
// return the replacement text from the userReplacementsDictionary first.
replacement = QString::fromNSString([replacements valueForKey:nsWord]);
lst << replacement;
}
for (NSString *suggestion in suggestions) {
// the replacement text from the userReplacementsDictionary will be in
// the suggestions list; don't add it again.
QString str = QString::fromNSString(suggestion);
if (str != replacement) {
lst << str;
}
}
return lst;
}
bool NSSpellCheckerDict::storeReplacement(const QString &bad,
const QString &good)
{
qCDebug(SONNET_NSSPELLCHECKER) << "Not storing replacement" << good << "for" << bad;
return false;
}
bool NSSpellCheckerDict::addToPersonal(const QString &word)
{
NSString *nsWord = word.toNSString();
NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker];
if (![checker hasLearnedWord:nsWord]) {
[checker learnWord:nsWord];
[checker updatePanels];
}
return true;
}
bool NSSpellCheckerDict::addToSession(const QString &word)
{
qCDebug(SONNET_NSSPELLCHECKER) << "Not storing" << word << "in the session dictionary";
return false;
}
@@ -0,0 +1,21 @@
add_library(sonnet_voikko MODULE
voikkoclient.cpp
voikkodict.cpp
)
ecm_qt_declare_logging_category(sonnet_voikko
HEADER voikkodebug.h
IDENTIFIER SONNET_VOIKKO
CATEGORY_NAME kf.sonnet.clients.voikko
OLD_CATEGORY_NAMES sonnet.plugins.voikko
DESCRIPTION "Sonnet Voikko plugin"
EXPORT SONNET
)
target_include_directories(sonnet_voikko PRIVATE ${VOIKKO_INCLUDE_DIR})
target_link_libraries(sonnet_voikko PRIVATE KF6::SonnetCore ${VOIKKO_LIBRARIES})
install(TARGETS sonnet_voikko DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf6/sonnet/)
set(SONNET_BACKEND_FOUND TRUE PARENT_SCOPE)
@@ -0,0 +1,63 @@
/*
* voikkoclient.cpp
*
* SPDX-FileCopyrightText: 2015 Jesse Jaara <jesse.jaara@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "voikkoclient.h"
#include "voikkodebug.h"
#include "voikkodict.h"
VoikkoClient::VoikkoClient(QObject *parent)
: Sonnet::Client(parent)
{
qCDebug(SONNET_VOIKKO) << "Initializing Voikko spell checker plugin.";
char **dictionaries = voikkoListSupportedSpellingLanguages(nullptr);
if (!dictionaries) {
return;
}
for (int i = 0; dictionaries[i] != nullptr; ++i) {
QString language = QString::fromUtf8(dictionaries[i]);
m_supportedLanguages.append(language);
qCDebug(SONNET_VOIKKO) << "Found dictionary for language:" << language;
}
voikkoFreeCstrArray(dictionaries);
}
VoikkoClient::~VoikkoClient()
{
}
int VoikkoClient::reliability() const
{
return 50;
}
Sonnet::SpellerPlugin *VoikkoClient::createSpeller(const QString &language)
{
VoikkoDict *speller = new VoikkoDict(language);
if (speller->initFailed()) {
delete speller;
return nullptr;
}
return speller;
}
QStringList VoikkoClient::languages() const
{
return m_supportedLanguages;
}
QString VoikkoClient::name() const
{
return QStringLiteral("Voikko");
}
#include "moc_voikkoclient.cpp"
@@ -0,0 +1,36 @@
/*
* voikkoclient.h
*
* SPDX-FileCopyrightText: 2015 Jesse Jaara <jesse.jaara@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_VOIKKOCLIENT_H
#define SONNET_VOIKKOCLIENT_H
#include "client_p.h"
class VoikkoClient : public Sonnet::Client
{
Q_OBJECT
Q_INTERFACES(Sonnet::Client)
Q_PLUGIN_METADATA(IID "org.kde.Sonnet.VoikkoClient")
public:
explicit VoikkoClient(QObject *parent = nullptr);
~VoikkoClient();
int reliability() const override;
Sonnet::SpellerPlugin *createSpeller(const QString &language) override;
QStringList languages() const override;
QString name() const override;
private:
QStringList m_supportedLanguages;
};
#endif // SONNET_VOIKKOCLIENT_H
@@ -0,0 +1,307 @@
/*
* voikkodict.cpp
*
* SPDX-FileCopyrightText: 2015 Jesse Jaara <jesse.jaara@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "voikkodict.h"
#include "voikkodebug.h"
#include <QDir>
#include <QList>
#include <QStandardPaths>
#ifdef Q_IS_WIN
#include <QSysInfo>
#endif
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace
{
// QString literals used in loading and storing user dictionary
inline const QString replacement_bad_str() Q_DECL_NOEXCEPT
{
return QStringLiteral("bad");
}
inline const QString replacement_good_str() Q_DECL_NOEXCEPT
{
return QStringLiteral("good");
}
inline const QString personal_words_str() Q_DECL_NOEXCEPT
{
return QStringLiteral("PersonalWords");
}
inline const QString replacements_str() Q_DECL_NOEXCEPT
{
return QStringLiteral("Replacements");
}
// Set path to: QStandardPaths::GenericDataLocation/Sonnet/Voikko-user-dictionary.json
QString getUserDictionaryPath() Q_DECL_NOEXCEPT
{
QString directory = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
#ifdef Q_OS_WIN
// Resolve the windows' Roaming directory manually
if (QSysInfo::windowsVersion() == QSysInfo::WV_XP || QSysInfo::windowsVersion() == QSysInfo::WV_2003) {
// In Xp Roaming is "<user>/Application Data"
// DataLocation: "<user>/Local Settings/Application Data"
directory += QStringLiteral("/../../Application Data");
} else {
directory += QStringLiteral("/../Roaming");
}
#endif
directory += QStringLiteral("/Sonnet");
QDir path(directory);
path.mkpath(path.absolutePath());
return path.absoluteFilePath(QStringLiteral("Voikko-user-dictionary.json"));
}
void addReplacementToNode(QJsonObject &languageNode, const QString &bad, const QString &good) Q_DECL_NOEXCEPT
{
QJsonObject pair;
pair[replacement_bad_str()] = good;
pair[replacement_good_str()] = bad;
auto replaceList = languageNode[replacements_str()].toArray();
replaceList.append(pair);
languageNode[replacements_str()] = replaceList;
}
void addPersonalWordToNode(QJsonObject &languageNode, const QString &word) Q_DECL_NOEXCEPT
{
auto arr = languageNode[personal_words_str()].toArray();
arr.append(word);
languageNode[personal_words_str()] = arr;
}
/**
* Read and return the root json object from fileName.
*
* Returns an empty node in case of an IO error or the file is empty.
*/
QJsonObject readJsonRootObject(const QString &fileName) Q_DECL_NOEXCEPT
{
QFile userDictFile(fileName);
if (!userDictFile.exists()) {
return QJsonObject(); // Nothing has been saved so far.
}
if (!userDictFile.open(QIODevice::ReadOnly)) {
qCWarning(SONNET_VOIKKO) << "Could not open personal dictionary. Failed to open file" << fileName;
qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString();
return QJsonObject();
}
QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll());
userDictFile.close();
return dictDoc.object();
}
}
class VoikkoDictPrivate
{
public:
VoikkoHandle *m_handle;
const VoikkoDict *q;
QSet<QString> m_sessionWords;
QSet<QString> m_personalWords;
QHash<QString, QString> m_replacements;
QString m_userDictionaryFilepath;
// Used when converting Qstring to wchar_t strings
QList<wchar_t> m_conversionBuffer;
VoikkoDictPrivate(const QString &language, const VoikkoDict *publicPart) Q_DECL_NOEXCEPT : q(publicPart),
m_userDictionaryFilepath(getUserDictionaryPath()),
m_conversionBuffer(256)
{
const char *error;
m_handle = voikkoInit(&error, language.toUtf8().data(), nullptr);
if (error != nullptr) {
qCWarning(SONNET_VOIKKO) << "Failed to initialize Voikko spelling backend. Reason:" << error;
} else { // Continue to load user's own words
loadUserDictionary();
}
}
/**
* Store a new ignored/personal word or replacement pair in the user's
* dictionary m_userDictionaryFilepath.
*
* returns true on success else false
*/
bool storePersonal(const QString &personalWord, const QString &bad = QString(), const QString &good = QString()) const Q_DECL_NOEXCEPT
{
QFile userDictFile(m_userDictionaryFilepath);
if (!userDictFile.open(QIODevice::ReadWrite)) {
qCWarning(SONNET_VOIKKO) << "Could not save personal dictionary. Failed to open file:" << m_userDictionaryFilepath;
qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString();
return false;
}
QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll());
auto root = readJsonRootObject(m_userDictionaryFilepath);
auto languageNode = root[q->language()].toObject();
// Empty value means we are storing a bad:good pair
if (personalWord.isEmpty()) {
addReplacementToNode(languageNode, bad, good);
} else {
addPersonalWordToNode(languageNode, personalWord);
}
root[q->language()] = languageNode;
dictDoc.setObject(root);
userDictFile.reset();
userDictFile.write(dictDoc.toJson());
userDictFile.close();
qCDebug(SONNET_VOIKKO) << "Changes to user dictionary saved to file: " << m_userDictionaryFilepath;
return true;
}
/**
* Load user's own personal words and replacement pairs from
* m_userDictionaryFilepath.
*/
void loadUserDictionary() Q_DECL_NOEXCEPT
{
// If root is empty we will fail later on when checking if
// languageNode is empty.
auto root = readJsonRootObject(m_userDictionaryFilepath);
auto languageNode = root[q->language()].toObject();
if (languageNode.isEmpty()) {
return; // Nothing to load
}
loadUserWords(languageNode);
loadUserReplacements(languageNode);
}
/**
* Convert the given QString to a \0 terminated wchar_t string.
* Uses QList as a buffer and return it's internal data pointer.
*/
inline const wchar_t *QStringToWchar(const QString &str) Q_DECL_NOEXCEPT
{
m_conversionBuffer.resize(str.length() + 1);
int size = str.toWCharArray(m_conversionBuffer.data());
m_conversionBuffer[size] = '\0';
return m_conversionBuffer.constData();
}
private:
/**
* Extract and append user defined words from the languageNode.
*/
inline void loadUserWords(const QJsonObject &languageNode) Q_DECL_NOEXCEPT
{
const auto words = languageNode[personal_words_str()].toArray();
for (auto word : words) {
m_personalWords.insert(word.toString());
}
qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 words from the user dictionary.").arg(words.size());
}
/**
* Extract and append user defined replacement pairs from the languageNode.
*/
inline void loadUserReplacements(const QJsonObject &languageNode) Q_DECL_NOEXCEPT
{
const auto words = languageNode[replacements_str()].toArray();
for (auto pair : words) {
m_replacements[pair.toObject()[replacement_bad_str()].toString()] = pair.toObject()[replacement_good_str()].toString();
}
qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 replacements from the user dictionary.").arg(words.size());
}
};
VoikkoDict::VoikkoDict(const QString &language) Q_DECL_NOEXCEPT : SpellerPlugin(language), d(new VoikkoDictPrivate(language, this))
{
qCDebug(SONNET_VOIKKO) << "Loading dictionary for language:" << language;
}
VoikkoDict::~VoikkoDict()
{
}
bool VoikkoDict::isCorrect(const QString &word) const
{
// Check the session word list and personal word list first
if (d->m_sessionWords.contains(word) || d->m_personalWords.contains(word)) {
return true;
}
return voikkoSpellUcs4(d->m_handle, d->QStringToWchar(word)) == VOIKKO_SPELL_OK;
}
QStringList VoikkoDict::suggest(const QString &word) const
{
QStringList suggestions;
auto userDictPos = d->m_replacements.constFind(word);
if (userDictPos != d->m_replacements.constEnd()) {
suggestions.append(*userDictPos);
}
auto voikkoSuggestions = voikkoSuggestUcs4(d->m_handle, d->QStringToWchar(word));
if (!voikkoSuggestions) {
return suggestions;
}
for (int i = 0; voikkoSuggestions[i] != nullptr; ++i) {
QString suggestion = QString::fromWCharArray(voikkoSuggestions[i]);
suggestions.append(suggestion);
}
qCDebug(SONNET_VOIKKO) << "Misspelled:" << word << "|Suggestons:" << suggestions.join(QLatin1String(", "));
voikko_free_suggest_ucs4(voikkoSuggestions);
return suggestions;
}
bool VoikkoDict::storeReplacement(const QString &bad, const QString &good)
{
qCDebug(SONNET_VOIKKO) << "Adding new replacement pair to user dictionary:" << bad << "->" << good;
d->m_replacements[bad] = good;
return d->storePersonal(QString(), bad, good);
}
bool VoikkoDict::addToPersonal(const QString &word)
{
qCDebug(SONNET_VOIKKO()) << "Adding new word to user dictionary" << word;
d->m_personalWords.insert(word);
return d->storePersonal(word);
}
bool VoikkoDict::addToSession(const QString &word)
{
qCDebug(SONNET_VOIKKO()) << "Adding new word to session dictionary" << word;
d->m_sessionWords.insert(word);
return true;
}
bool VoikkoDict::initFailed() const Q_DECL_NOEXCEPT
{
return !d->m_handle;
}
@@ -0,0 +1,54 @@
/*
* voikkodict.h
*
* SPDX-FileCopyrightText: 2015 Jesse Jaara <jesse.jaara@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_VOIKKODICT_H
#define SONNET_VOIKKODICT_H
#include "spellerplugin_p.h"
#include <libvoikko/voikko.h>
#include <QHash>
#include <QScopedPointer>
class VoikkoClient;
class VoikkoDictPrivate;
class VoikkoDict : public Sonnet::SpellerPlugin
{
public:
/**
* Declare VoikkoClient as friend so we can use the protected constructor.
*/
friend class VoikkoClient;
~VoikkoDict();
bool isCorrect(const QString &word) const override;
QStringList suggest(const QString &word) const override;
bool storeReplacement(const QString &bad, const QString &good) override;
bool addToPersonal(const QString &word) override;
bool addToSession(const QString &word) override;
/**
* @returns true if initializing Voikko backend failed.
*/
bool initFailed() const Q_DECL_NOEXCEPT;
protected:
/**
* Constructor is protected so that only spellers created
* and validated through VoikkoClient can be used.
*/
explicit VoikkoDict(const QString &language) Q_DECL_NOEXCEPT;
private:
QScopedPointer<VoikkoDictPrivate> d;
};
#endif // SONNET_VOIKKODICT_H
@@ -0,0 +1,21 @@
ecm_add_qml_module(sonnetquickplugin URI "org.kde.sonnet" VERSION 1.0 GENERATE_PLUGIN_SOURCE DEPENDENCIES QtQuick)
target_sources(sonnetquickplugin PRIVATE
types.h
spellcheckhighlighter.cpp
spellcheckhighlighter.h )
ecm_qt_declare_logging_category(sonnetquickplugin
HEADER quick_debug.h
IDENTIFIER SONNET_LOG_QUICK
CATEGORY_NAME kf.sonnet.quick
OLD_CATEGORY_NAMES sonnet.quick
DESCRIPTION "Sonnet Quick bindings"
EXPORT SONNET
)
target_link_libraries(sonnetquickplugin
PUBLIC Qt6::Quick
PRIVATE KF6SonnetCore
)
ecm_finalize_qml_module(sonnetquickplugin DESTINATION ${KDE_INSTALL_QMLDIR})
@@ -0,0 +1,702 @@
// SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
// SPDX-FileCopyrightText: 2020 Christian Mollekopf <mollekopf@kolabsystems.com>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "spellcheckhighlighter.h"
#include "guesslanguage.h"
#include "languagefilter_p.h"
#include "loader_p.h"
#include "settingsimpl_p.h"
#include "speller.h"
#include "tokenizer_p.h"
#include "quick_debug.h"
#include <QColor>
#include <QHash>
#include <QKeyEvent>
#include <QMetaMethod>
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <QTextCursor>
#include <QTimer>
#include <memory>
using namespace Sonnet;
// Cache of previously-determined languages (when using AutoDetectLanguage)
// There is one such cache per block (paragraph)
class LanguageCache : public QTextBlockUserData
{
public:
// Key: QPair<start, length>
// Value: language name
QMap<QPair<int, int>, QString> languages;
// Remove all cached language information after @p pos
void invalidate(int pos)
{
QMutableMapIterator<QPair<int, int>, QString> it(languages);
it.toBack();
while (it.hasPrevious()) {
it.previous();
if (it.key().first + it.key().second >= pos) {
it.remove();
} else {
break;
}
}
}
QString languageAtPos(int pos) const
{
// The data structure isn't really great for such lookups...
QMapIterator<QPair<int, int>, QString> it(languages);
while (it.hasNext()) {
it.next();
if (it.key().first <= pos && it.key().first + it.key().second >= pos) {
return it.value();
}
}
return QString();
}
};
class HighlighterPrivate
{
public:
HighlighterPrivate(SpellcheckHighlighter *qq)
: q(qq)
{
tokenizer = std::make_unique<WordTokenizer>();
active = true;
automatic = false;
autoDetectLanguageDisabled = false;
connected = false;
wordCount = 0;
errorCount = 0;
intraWordEditing = false;
completeRehighlightRequired = false;
spellColor = spellColor.isValid() ? spellColor : Qt::red;
languageFilter = std::make_unique<LanguageFilter>(new SentenceTokenizer());
loader = Loader::openLoader();
loader->settings()->restore();
spellchecker = std::make_unique<Speller>();
spellCheckerFound = spellchecker->isValid();
rehighlightRequest = new QTimer(q);
q->connect(rehighlightRequest, &QTimer::timeout, q, &SpellcheckHighlighter::slotRehighlight);
if (!spellCheckerFound) {
return;
}
disablePercentage = loader->settings()->disablePercentageWordError();
disableWordCount = loader->settings()->disableWordErrorCount();
completeRehighlightRequired = true;
rehighlightRequest->setInterval(0);
rehighlightRequest->setSingleShot(true);
rehighlightRequest->start();
// Danger red from our color scheme
errorFormat.setForeground(spellColor);
errorFormat.setUnderlineColor(spellColor);
errorFormat.setUnderlineStyle(QTextCharFormat::SingleUnderline);
selectedErrorFormat.setForeground(spellColor);
auto bg = spellColor;
bg.setAlphaF(0.1);
selectedErrorFormat.setBackground(bg);
selectedErrorFormat.setUnderlineColor(spellColor);
selectedErrorFormat.setUnderlineStyle(QTextCharFormat::SingleUnderline);
quoteFormat.setForeground(QColor{"#7f8c8d"});
}
~HighlighterPrivate();
std::unique_ptr<WordTokenizer> tokenizer;
std::unique_ptr<LanguageFilter> languageFilter;
Loader *loader = nullptr;
std::unique_ptr<Speller> spellchecker;
QTextCharFormat errorFormat;
QTextCharFormat selectedErrorFormat;
QTextCharFormat quoteFormat;
std::unique_ptr<Sonnet::GuessLanguage> languageGuesser;
QString selectedWord;
QQuickTextDocument *document = nullptr;
int cursorPosition = 0;
int selectionStart = 0;
int selectionEnd = 0;
int autoCompleteBeginPosition = -1;
int autoCompleteEndPosition = -1;
int wordIsMisspelled = false;
bool active = false;
bool automatic = false;
bool autoDetectLanguageDisabled = false;
bool completeRehighlightRequired = false;
bool intraWordEditing = false;
bool spellCheckerFound = false; // cached d->dict->isValid() value
bool connected = false;
int disablePercentage = 0;
int disableWordCount = 0;
int wordCount = 0;
int errorCount = 0;
QTimer *rehighlightRequest = nullptr;
QColor spellColor;
SpellcheckHighlighter *const q;
};
HighlighterPrivate::~HighlighterPrivate()
{
}
SpellcheckHighlighter::SpellcheckHighlighter(QObject *parent)
: QSyntaxHighlighter(parent)
, d(new HighlighterPrivate(this))
{
}
SpellcheckHighlighter::~SpellcheckHighlighter()
{
if (document()) {
disconnect(document(), nullptr, this, nullptr);
}
}
bool SpellcheckHighlighter::spellCheckerFound() const
{
return d->spellCheckerFound;
}
void SpellcheckHighlighter::slotRehighlight()
{
if (d->completeRehighlightRequired) {
d->wordCount = 0;
d->errorCount = 0;
rehighlight();
} else {
// rehighlight the current para only (undo/redo safe)
QTextCursor cursor = textCursor();
if (cursor.hasSelection()) {
cursor.clearSelection();
}
cursor.insertText(QString());
}
// if (d->checksDone == d->checksRequested)
// d->completeRehighlightRequired = false;
QTimer::singleShot(0, this, &SpellcheckHighlighter::slotAutoDetection);
}
bool SpellcheckHighlighter::automatic() const
{
return d->automatic;
}
bool SpellcheckHighlighter::autoDetectLanguageDisabled() const
{
return d->autoDetectLanguageDisabled;
}
bool SpellcheckHighlighter::intraWordEditing() const
{
return d->intraWordEditing;
}
void SpellcheckHighlighter::setIntraWordEditing(bool editing)
{
d->intraWordEditing = editing;
}
void SpellcheckHighlighter::setAutomatic(bool automatic)
{
if (automatic == d->automatic) {
return;
}
d->automatic = automatic;
if (d->automatic) {
slotAutoDetection();
}
}
void SpellcheckHighlighter::setAutoDetectLanguageDisabled(bool autoDetectDisabled)
{
d->autoDetectLanguageDisabled = autoDetectDisabled;
}
void SpellcheckHighlighter::slotAutoDetection()
{
bool savedActive = d->active;
// don't disable just because 1 of 4 is misspelled.
if (d->automatic && d->wordCount >= 10) {
// tme = Too many errors
/* clang-format off */
bool tme = (d->errorCount >= d->disableWordCount)
&& (d->errorCount * 100 >= d->disablePercentage * d->wordCount);
/* clang-format on */
if (d->active && tme) {
d->active = false;
} else if (!d->active && !tme) {
d->active = true;
}
}
if (d->active != savedActive) {
if (d->active) {
Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
} else {
qCDebug(SONNET_LOG_QUICK) << "Sonnet: Disabling spell checking, too many errors";
Q_EMIT activeChanged(
tr("Too many misspelled words. "
"As-you-type spell checking disabled."));
}
d->completeRehighlightRequired = true;
d->rehighlightRequest->setInterval(100);
d->rehighlightRequest->setSingleShot(true);
}
}
void SpellcheckHighlighter::setActive(bool active)
{
if (active == d->active) {
return;
}
d->active = active;
Q_EMIT activeChanged();
rehighlight();
if (d->active) {
Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
} else {
Q_EMIT activeChanged(tr("As-you-type spell checking disabled."));
}
}
bool SpellcheckHighlighter::active() const
{
return d->active;
}
static bool hasNotEmptyText(const QString &text)
{
for (int i = 0; i < text.length(); ++i) {
if (!text.at(i).isSpace()) {
return true;
}
}
return false;
}
void SpellcheckHighlighter::contentsChange(int pos, int add, int rem)
{
// Invalidate the cache where the text has changed
const QTextBlock &lastBlock = document()->findBlock(pos + add - rem);
QTextBlock block = document()->findBlock(pos);
do {
LanguageCache *cache = dynamic_cast<LanguageCache *>(block.userData());
if (cache) {
cache->invalidate(pos - block.position());
}
block = block.next();
} while (block.isValid() && block < lastBlock);
}
void SpellcheckHighlighter::highlightBlock(const QString &text)
{
if (!hasNotEmptyText(text) || !d->active || !d->spellCheckerFound) {
return;
}
// Avoid spellchecking quotes
if (text.isEmpty() || text.at(0) == QLatin1Char('>')) {
setFormat(0, text.length(), d->quoteFormat);
return;
}
if (!d->connected) {
connect(textDocument(), &QTextDocument::contentsChange, this, &SpellcheckHighlighter::contentsChange);
d->connected = true;
}
QTextCursor cursor = textCursor();
const int index = cursor.position() + 1;
const int lengthPosition = text.length() - 1;
if (index != lengthPosition //
|| (lengthPosition > 0 && !text[lengthPosition - 1].isLetter())) {
d->languageFilter->setBuffer(text);
LanguageCache *cache = dynamic_cast<LanguageCache *>(currentBlockUserData());
if (!cache) {
cache = new LanguageCache;
setCurrentBlockUserData(cache);
}
const bool autodetectLanguage = d->spellchecker->testAttribute(Speller::AutoDetectLanguage);
while (d->languageFilter->hasNext()) {
Sonnet::Token sentence = d->languageFilter->next();
if (autodetectLanguage && !d->autoDetectLanguageDisabled) {
QString lang;
QPair<int, int> spos = QPair<int, int>(sentence.position(), sentence.length());
// try cache first
if (cache->languages.contains(spos)) {
lang = cache->languages.value(spos);
} else {
lang = d->languageFilter->language();
if (!d->languageFilter->isSpellcheckable()) {
lang.clear();
}
cache->languages[spos] = lang;
}
if (lang.isEmpty()) {
continue;
}
d->spellchecker->setLanguage(lang);
}
d->tokenizer->setBuffer(sentence.toString());
int offset = sentence.position();
while (d->tokenizer->hasNext()) {
Sonnet::Token word = d->tokenizer->next();
if (!d->tokenizer->isSpellcheckable()) {
continue;
}
++d->wordCount;
if (d->spellchecker->isMisspelled(word.toString())) {
++d->errorCount;
if (word.position() + offset <= cursor.position() && cursor.position() <= word.position() + offset + word.length()) {
setMisspelledSelected(word.position() + offset, word.length());
} else {
setMisspelled(word.position() + offset, word.length());
}
} else {
unsetMisspelled(word.position() + offset, word.length());
}
}
}
}
// QTimer::singleShot( 0, this, SLOT(checkWords()) );
setCurrentBlockState(0);
}
QStringList SpellcheckHighlighter::suggestions(int mousePosition, int max)
{
if (!textDocument()) {
return {};
}
Q_EMIT changeCursorPosition(mousePosition, mousePosition);
QTextCursor cursor = textCursor();
QTextCursor cursorAtMouse(textDocument());
cursorAtMouse.setPosition(mousePosition);
// Check if the user clicked a selected word
const bool selectedWordClicked = cursor.hasSelection() && mousePosition >= cursor.selectionStart() && mousePosition <= cursor.selectionEnd();
// Get the word under the (mouse-)cursor and see if it is misspelled.
// Don't include apostrophes at the start/end of the word in the selection.
QTextCursor wordSelectCursor(cursorAtMouse);
wordSelectCursor.clearSelection();
wordSelectCursor.select(QTextCursor::WordUnderCursor);
d->selectedWord = wordSelectCursor.selectedText();
// Clear the selection again, we re-select it below (without the apostrophes).
wordSelectCursor.setPosition(wordSelectCursor.position() - d->selectedWord.size());
if (d->selectedWord.startsWith(QLatin1Char('\'')) || d->selectedWord.startsWith(QLatin1Char('\"'))) {
d->selectedWord = d->selectedWord.right(d->selectedWord.size() - 1);
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
}
if (d->selectedWord.endsWith(QLatin1Char('\'')) || d->selectedWord.endsWith(QLatin1Char('\"'))) {
d->selectedWord.chop(1);
}
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, d->selectedWord.size());
int endSelection = wordSelectCursor.selectionEnd();
Q_EMIT wordUnderMouseChanged();
bool isMouseCursorInsideWord = true;
if ((mousePosition < wordSelectCursor.selectionStart() || mousePosition >= wordSelectCursor.selectionEnd()) //
&& (d->selectedWord.length() > 1)) {
isMouseCursorInsideWord = false;
}
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, d->selectedWord.size());
d->wordIsMisspelled = isMouseCursorInsideWord && !d->selectedWord.isEmpty() && d->spellchecker->isMisspelled(d->selectedWord);
Q_EMIT wordIsMisspelledChanged();
if (!d->wordIsMisspelled || selectedWordClicked) {
return QStringList{};
}
LanguageCache *cache = dynamic_cast<LanguageCache *>(cursor.block().userData());
if (cache) {
const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock());
if (!cachedLanguage.isEmpty()) {
d->spellchecker->setLanguage(cachedLanguage);
}
}
QStringList suggestions = d->spellchecker->suggest(d->selectedWord);
if (max >= 0 && suggestions.count() > max) {
suggestions = suggestions.mid(0, max);
}
return suggestions;
}
QString SpellcheckHighlighter::currentLanguage() const
{
return d->spellchecker->language();
}
void SpellcheckHighlighter::setCurrentLanguage(const QString &lang)
{
QString prevLang = d->spellchecker->language();
d->spellchecker->setLanguage(lang);
d->spellCheckerFound = d->spellchecker->isValid();
if (!d->spellCheckerFound) {
qCDebug(SONNET_LOG_QUICK) << "No dictionary for \"" << lang << "\" staying with the current language.";
d->spellchecker->setLanguage(prevLang);
return;
}
d->wordCount = 0;
d->errorCount = 0;
if (d->automatic || d->active) {
d->rehighlightRequest->start(0);
}
}
void SpellcheckHighlighter::setMisspelled(int start, int count)
{
setFormat(start, count, d->errorFormat);
}
void SpellcheckHighlighter::setMisspelledSelected(int start, int count)
{
setFormat(start, count, d->selectedErrorFormat);
}
void SpellcheckHighlighter::unsetMisspelled(int start, int count)
{
setFormat(start, count, QTextCharFormat());
}
void SpellcheckHighlighter::addWordToDictionary(const QString &word)
{
d->spellchecker->addToPersonal(word);
rehighlight();
}
void SpellcheckHighlighter::ignoreWord(const QString &word)
{
d->spellchecker->addToSession(word);
rehighlight();
}
void SpellcheckHighlighter::replaceWord(const QString &replacement, int at)
{
QTextCursor textCursorUnderUserCursor(textDocument());
textCursorUnderUserCursor.setPosition(at == -1 ? d->cursorPosition : at);
// Get the word under the cursor
QTextCursor wordSelectCursor(textCursorUnderUserCursor);
wordSelectCursor.clearSelection();
wordSelectCursor.select(QTextCursor::WordUnderCursor);
auto selectedWord = wordSelectCursor.selectedText();
// Trim leading and trailing apostrophes
wordSelectCursor.setPosition(wordSelectCursor.position() - selectedWord.size());
if (selectedWord.startsWith(QLatin1Char('\'')) || selectedWord.startsWith(QLatin1Char('\"'))) {
selectedWord = selectedWord.right(selectedWord.size() - 1);
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
}
if (selectedWord.endsWith(QLatin1Char('\'')) || d->selectedWord.endsWith(QLatin1Char('\"'))) {
selectedWord.chop(1);
}
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, d->selectedWord.size());
wordSelectCursor.insertText(replacement);
}
QQuickTextDocument *SpellcheckHighlighter::quickDocument() const
{
return d->document;
}
void SpellcheckHighlighter::setQuickDocument(QQuickTextDocument *document)
{
if (document == d->document) {
return;
}
if (d->document) {
d->document->parent()->removeEventFilter(this);
d->document->textDocument()->disconnect(this);
}
d->document = document;
document->parent()->installEventFilter(this);
setDocument(document->textDocument());
Q_EMIT documentChanged();
}
void SpellcheckHighlighter::setDocument(QTextDocument *document)
{
d->connected = false;
QSyntaxHighlighter::setDocument(document);
}
int SpellcheckHighlighter::cursorPosition() const
{
return d->cursorPosition;
}
void SpellcheckHighlighter::setCursorPosition(int position)
{
if (position == d->cursorPosition) {
return;
}
d->cursorPosition = position;
d->rehighlightRequest->start(0);
Q_EMIT cursorPositionChanged();
}
int SpellcheckHighlighter::selectionStart() const
{
return d->selectionStart;
}
void SpellcheckHighlighter::setSelectionStart(int position)
{
if (position == d->selectionStart) {
return;
}
d->selectionStart = position;
Q_EMIT selectionStartChanged();
}
int SpellcheckHighlighter::selectionEnd() const
{
return d->selectionEnd;
}
void SpellcheckHighlighter::setSelectionEnd(int position)
{
if (position == d->selectionEnd) {
return;
}
d->selectionEnd = position;
Q_EMIT selectionEndChanged();
}
QTextCursor SpellcheckHighlighter::textCursor() const
{
QTextDocument *doc = textDocument();
if (!doc) {
return QTextCursor();
}
QTextCursor cursor(doc);
if (d->selectionStart != d->selectionEnd) {
cursor.setPosition(d->selectionStart);
cursor.setPosition(d->selectionEnd, QTextCursor::KeepAnchor);
} else {
cursor.setPosition(d->cursorPosition);
}
return cursor;
}
QTextDocument *SpellcheckHighlighter::textDocument() const
{
if (!d->document) {
return nullptr;
}
return d->document->textDocument();
}
bool SpellcheckHighlighter::wordIsMisspelled() const
{
return d->wordIsMisspelled;
}
QString SpellcheckHighlighter::wordUnderMouse() const
{
return d->selectedWord;
}
QColor SpellcheckHighlighter::misspelledColor() const
{
return d->spellColor;
}
void SpellcheckHighlighter::setMisspelledColor(const QColor &color)
{
if (color == d->spellColor) {
return;
}
d->spellColor = color;
Q_EMIT misspelledColorChanged();
}
bool SpellcheckHighlighter::isWordMisspelled(const QString &word)
{
return d->spellchecker->isMisspelled(word);
}
bool SpellcheckHighlighter::eventFilter(QObject *o, QEvent *e)
{
if (!d->spellCheckerFound) {
return false;
}
if (o == d->document->parent() && (e->type() == QEvent::KeyPress)) {
QKeyEvent *k = static_cast<QKeyEvent *>(e);
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return || k->key() == Qt::Key_Up || k->key() == Qt::Key_Down || k->key() == Qt::Key_Left
|| k->key() == Qt::Key_Right || k->key() == Qt::Key_PageUp || k->key() == Qt::Key_PageDown || k->key() == Qt::Key_Home || k->key() == Qt::Key_End
|| (k->modifiers() == Qt::ControlModifier
&& (k->key() == Qt::Key_A || k->key() == Qt::Key_B || k->key() == Qt::Key_E || k->key() == Qt::Key_N
|| k->key() == Qt::Key_P))) { /* clang-format on */
if (intraWordEditing()) {
setIntraWordEditing(false);
d->completeRehighlightRequired = true;
d->rehighlightRequest->setInterval(500);
d->rehighlightRequest->setSingleShot(true);
d->rehighlightRequest->start();
}
} else {
setIntraWordEditing(true);
}
if (k->key() == Qt::Key_Space //
|| k->key() == Qt::Key_Enter //
|| k->key() == Qt::Key_Return) {
QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
}
} else if (d->document && e->type() == QEvent::MouseButtonPress) {
if (intraWordEditing()) {
setIntraWordEditing(false);
d->completeRehighlightRequired = true;
d->rehighlightRequest->setInterval(0);
d->rehighlightRequest->setSingleShot(true);
d->rehighlightRequest->start();
}
}
return false;
}
#include "moc_spellcheckhighlighter.cpp"
@@ -0,0 +1,241 @@
// SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
// SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@kde.org>
// SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
// SPDX-FileCopyrightText: 2020 Christian Mollekopf <mollekopf@kolabsystems.com>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
// TODO KF6 create AbstractSpellcheckHighlighter and make the QtQuick and QtWidget inherit
// from it.
#include <QQuickTextDocument>
#include <QSyntaxHighlighter>
class HighlighterPrivate;
/// \brief The Sonnet Highlighter class, used for drawing red lines in text fields
/// when detecting spelling mistakes.
///
/// SpellcheckHighlighter is adapted for QML applications. In usual Kirigami/QQC2-desktop-style
/// applications, this can be used directly by adding `Kirigami.SpellCheck.enabled: true` on
/// a TextArea.
///
/// On other QML applications, you can add the SpellcheckHighlighter as a child of a TextArea.
///
/// Note: TextField is not supported, as it lacks QTextDocument API that Sonnet relies on.
///
/// \code{.qml}
/// TextArea {
/// id: textArea
/// Sonnet.SpellcheckHighlighter {
/// id: spellcheckhighlighter
/// document: textArea.textDocument
/// cursorPosition: textArea.cursorPosition
/// selectionStart: textArea.selectionStart
/// selectionEnd: textArea.selectionEnd
/// misspelledColor: Kirigami.Theme.negativeTextColor
/// active: true
///
/// onChangeCursorPosition: {
/// textArea.cursorPosition = start;
/// textArea.moveCursorSelection(end, TextEdit.SelectCharacters);
/// }
/// }
/// }
/// \endcode
///
/// Additionally SpellcheckHighlighter provides some convenient methods to create
/// a context menu with suggestions. \see suggestions
///
/// \since 5.88
class SpellcheckHighlighter : public QSyntaxHighlighter
{
Q_OBJECT
QML_ELEMENT
/// This property holds the underneath document from a QML TextEdit.
/// \since 5.88
Q_PROPERTY(QQuickTextDocument *document READ quickDocument WRITE setQuickDocument NOTIFY documentChanged)
/// This property holds the current cursor position.
/// \since 5.88
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
/// This property holds the start of the selection.
/// \since 5.88
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
/// This property holds the end of the selection.
/// \since 5.88
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
/// This property holds whether the current word under the mouse is misspelled.
/// \since 5.88
Q_PROPERTY(bool wordIsMisspelled READ wordIsMisspelled NOTIFY wordIsMisspelledChanged)
/// This property holds the current word under the mouse.
/// \since 5.88
Q_PROPERTY(QString wordUnderMouse READ wordUnderMouse NOTIFY wordUnderMouseChanged)
/// This property holds the spell color. By default, it's red.
/// \since 5.88
Q_PROPERTY(QColor misspelledColor READ misspelledColor WRITE setMisspelledColor NOTIFY misspelledColorChanged)
/// This property holds the current language used for spell checking.
/// \since 5.88
Q_PROPERTY(QString currentLanguage READ currentLanguage NOTIFY currentLanguageChanged)
/// This property holds whether a spell checking backend with support for the
/// \ref currentLanguage was found.
/// \since 5.88
Q_PROPERTY(bool spellCheckerFound READ spellCheckerFound CONSTANT)
/// \brief This property holds whether spell checking is enabled.
///
/// If \p active is true then spell checking is enabled; otherwise it
/// is disabled. Note that you have to disable automatic (de)activation
/// with \ref automatic before you change the state of spell
/// checking if you want to persistently enable/disable spell
/// checking.
///
/// \see automatic
/// \since 5.88
Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged)
/// This property holds whether spell checking is automatically disabled
/// if there's too many errors.
/// \since 5.88
Q_PROPERTY(bool automatic READ automatic WRITE setAutomatic NOTIFY automaticChanged)
/// This property holds whether the automatic language detection is disabled
/// overriding the Sonnet global settings.
/// \since 5.88
Q_PROPERTY(bool autoDetectLanguageDisabled READ autoDetectLanguageDisabled WRITE setAutoDetectLanguageDisabled NOTIFY autoDetectLanguageDisabledChanged)
public:
explicit SpellcheckHighlighter(QObject *parent = nullptr);
~SpellcheckHighlighter() override;
/// Returns a list of suggested replacements for the given misspelled word.
/// If the word is not misspelled, the list will be empty.
///
/// \param word the misspelled word
/// \param max at most this many suggestions will be returned. If this is
/// -1, as many suggestions as the spell backend supports will
/// be returned.
/// \return a list of suggested replacements for the word
/// \since 5.88
Q_INVOKABLE QStringList suggestions(int position, int max = 5);
/// Ignores the given word. This word will not be marked misspelled for
/// this session. It will again be marked as misspelled when creating
/// new highlighters.
///
/// \param word the word which will be ignored
/// \since 5.88
Q_INVOKABLE void ignoreWord(const QString &word);
/// Adds the given word permanently to the dictionary. It will never
/// be marked as misspelled again, even after restarting the application.
///
/// \param word the word which will be added to the dictionary
/// \since 5.88
Q_INVOKABLE void addWordToDictionary(const QString &word);
/// Replace word at the current cursor position, or @param at if
/// @param at is not -1.
/// \since 5.88
Q_INVOKABLE void replaceWord(const QString &word, int at = -1);
/// Checks if a given word is marked as misspelled by the highlighter.
///
/// \param word the word to be checked
/// \return true if the given word is misspelled.
/// \since 5.88
Q_INVOKABLE bool isWordMisspelled(const QString &word);
Q_REQUIRED_RESULT QQuickTextDocument *quickDocument() const;
void setQuickDocument(QQuickTextDocument *document);
Q_REQUIRED_RESULT int cursorPosition() const;
void setCursorPosition(int position);
Q_REQUIRED_RESULT int selectionStart() const;
void setSelectionStart(int position);
Q_REQUIRED_RESULT int selectionEnd() const;
void setSelectionEnd(int position);
Q_REQUIRED_RESULT bool wordIsMisspelled() const;
Q_REQUIRED_RESULT QString wordUnderMouse() const;
Q_REQUIRED_RESULT bool spellCheckerFound() const;
Q_REQUIRED_RESULT QString currentLanguage() const;
void setActive(bool active);
Q_REQUIRED_RESULT bool active() const;
void setAutomatic(bool automatic);
Q_REQUIRED_RESULT bool automatic() const;
void setAutoDetectLanguageDisabled(bool autoDetectDisabled);
Q_REQUIRED_RESULT bool autoDetectLanguageDisabled() const;
void setMisspelledColor(const QColor &color);
Q_REQUIRED_RESULT QColor misspelledColor() const;
void setQuoteColor(const QColor &color);
Q_REQUIRED_RESULT QColor quoteColor() const;
/// Set a new @ref QTextDocument for this highlighter to operate on.
/// \param document the new document to operate on.
/// \since 5.88
void setDocument(QTextDocument *document);
Q_SIGNALS:
void documentChanged();
void cursorPositionChanged();
void selectionStartChanged();
void selectionEndChanged();
void wordIsMisspelledChanged();
void wordUnderMouseChanged();
void changeCursorPosition(int start, int end);
void activeChanged();
void misspelledColorChanged();
void autoDetectLanguageDisabledChanged();
void automaticChanged();
void currentLanguageChanged();
/// Emitted when as-you-type spell checking is enabled or disabled.
///
/// \param description is a i18n description of the new state,
/// with an optional reason
/// \since 5.88
void activeChanged(const QString &description);
protected:
void highlightBlock(const QString &text) override;
virtual void setMisspelled(int start, int count);
virtual void setMisspelledSelected(int start, int count);
virtual void unsetMisspelled(int start, int count);
bool eventFilter(QObject *o, QEvent *e) override;
bool intraWordEditing() const;
void setIntraWordEditing(bool editing);
public Q_SLOTS:
/// Set language to use for spell checking.
///
/// \param language the language code for the new language to use.
/// \since 5.88
void setCurrentLanguage(const QString &language);
/// Run auto detection, disabling spell checking if too many errors are found.
/// \since 5.88
void slotAutoDetection();
/// Force a new highlighting.
/// \since 5.88
void slotRehighlight();
private:
Q_REQUIRED_RESULT QTextCursor textCursor() const;
Q_REQUIRED_RESULT QTextDocument *textDocument() const;
void contentsChange(int pos, int add, int rem);
void autodetectLanguage(const QString &sentence);
HighlighterPrivate *const d;
Q_DISABLE_COPY(SpellcheckHighlighter)
};
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <QQmlEngine>
#include <Sonnet/Settings>
struct SettingsForeign {
Q_GADGET
QML_ELEMENT
QML_NAMED_ELEMENT(Settings)
QML_FOREIGN(Sonnet::Settings)
};
@@ -0,0 +1,112 @@
add_library(KF6SonnetUi)
add_library(KF6::SonnetUi ALIAS KF6SonnetUi)
set_target_properties(KF6SonnetUi PROPERTIES
VERSION ${SONNET_VERSION}
SOVERSION ${SONNET_SOVERSION}
EXPORT_NAME SonnetUi
)
ecm_create_qm_loader(KF6SonnetUi sonnet6_qt)
qt_wrap_ui(sonnetui_ui_SRCS
configui.ui
sonnetui.ui
)
target_sources(KF6SonnetUi PRIVATE
${sonnetui_ui_SRCS}
configdialog.cpp
configview.cpp
configwidget.cpp
dialog.cpp
dictionarycombobox.cpp
highlighter.cpp
spellcheckdecorator.cpp
)
ecm_qt_declare_logging_category(KF6SonnetUi
HEADER ui_debug.h
IDENTIFIER SONNET_LOG_UI
CATEGORY_NAME kf.sonnet.ui
OLD_CATEGORY_NAMES sonnet.ui
DESCRIPTION "Sonnet UI"
EXPORT SONNET
)
ecm_generate_headers(SonnetUi_CamelCase_HEADERS
HEADER_NAMES
Dialog
Highlighter
ConfigDialog
ConfigView
ConfigWidget
DictionaryComboBox
SpellCheckDecorator
PREFIX Sonnet
REQUIRED_HEADERS SonnetUi_HEADERS
)
ecm_generate_export_header(KF6SonnetUi
BASE_NAME SonnetUi
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
VERSION_BASE_NAME Sonnet
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
)
target_link_libraries(KF6SonnetUi
PUBLIC Qt6::Widgets
PRIVATE KF6::SonnetCore
)
target_include_directories(KF6SonnetUi
INTERFACE
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/SonnetUi>"
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/Sonnet>"
PUBLIC
"$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}>" # version header
)
install(TARGETS KF6SonnetUi EXPORT KF6SonnetTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES ${SonnetUi_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/SonnetUi/Sonnet COMPONENT Devel)
install(FILES
${SonnetUi_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/sonnetui_export.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/SonnetUi/sonnet COMPONENT Devel)
if(BUILD_DESIGNERPLUGIN)
add_subdirectory(designer)
endif()
if (BUILD_QCH)
ecm_add_qch(
KF6SonnetUi_QCH
NAME SonnetUi
BASE_NAME KF6SonnetUi
VERSION ${KF_VERSION}
ORG_DOMAIN org.kde
SOURCES # using only public headers, to cover only public API
${SonnetUi_HEADERS}
LINK_QCHS
Qt6Core_QCH
Qt6Gui_QCH
Qt6Widgets_QCH
INCLUDE_DIRS
${CMAKE_CURRENT_BINARY_DIR}
BLANK_MACROS
SONNETUI_EXPORT
SONNETUI_DEPRECATED_EXPORT
SONNETUI_DEPRECATED
"SONNETUI_DEPRECATED_VERSION(x, y, t)"
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
COMPONENT Devel
)
endif()
@@ -0,0 +1,81 @@
/*
* configdialog.cpp
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "configdialog.h"
#include "configwidget.h"
#include <QDialogButtonBox>
#include <QVBoxLayout>
using namespace Sonnet;
class Sonnet::ConfigDialogPrivate
{
public:
ConfigDialogPrivate(ConfigDialog *parent)
: q(parent)
{
}
ConfigWidget *ui = nullptr;
ConfigDialog *const q;
void slotConfigChanged();
};
void ConfigDialogPrivate::slotConfigChanged()
{
Q_EMIT q->languageChanged(ui->language());
}
ConfigDialog::ConfigDialog(QWidget *parent)
: QDialog(parent)
, d(new ConfigDialogPrivate(this))
{
setObjectName(QStringLiteral("SonnetConfigDialog"));
setModal(true);
setWindowTitle(tr("Spell Checking Configuration"));
QVBoxLayout *layout = new QVBoxLayout(this);
d->ui = new ConfigWidget(this);
layout->addWidget(d->ui);
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, this, &ConfigDialog::slotOk);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(d->ui, SIGNAL(configChanged()), this, SLOT(slotConfigChanged()));
connect(d->ui, &ConfigWidget::configChanged, this, &ConfigDialog::configChanged);
}
ConfigDialog::~ConfigDialog() = default;
void ConfigDialog::slotOk()
{
d->ui->save();
accept();
}
void ConfigDialog::slotApply()
{
d->ui->save();
}
void ConfigDialog::setLanguage(const QString &language)
{
d->ui->setLanguage(language);
}
QString ConfigDialog::language() const
{
return d->ui->language();
}
#include "moc_configdialog.cpp"
@@ -0,0 +1,70 @@
/*
* configdialog.h
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_CONFIGDIALOG_H
#define SONNET_CONFIGDIALOG_H
#include "sonnetui_export.h"
#include <QDialog>
#include <memory>
namespace Sonnet
{
class ConfigDialogPrivate;
/// The sonnet ConfigDialog
class SONNETUI_EXPORT ConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit ConfigDialog(QWidget *parent);
~ConfigDialog() override;
/**
* Sets the language/dictionary that will be selected by default
* in this config dialog.
* This overrides the setting in the config file.
*
* @param language the language which will be selected by default.
* @since 4.1
*/
void setLanguage(const QString &language);
/**
* return selected language
* @since 4.8.1
*/
QString language() const;
protected Q_SLOTS:
virtual void slotOk();
virtual void slotApply();
Q_SIGNALS:
/**
* This is emitted all the time when we change config and not just language
*
* @param language the language which the user has selected
* @since 4.1
*/
void languageChanged(const QString &language);
/**
* This is emitted when configChanged
* @since 4.8.1
*/
void configChanged();
private:
std::unique_ptr<ConfigDialogPrivate> const d;
Q_DISABLE_COPY(ConfigDialog)
Q_PRIVATE_SLOT(d, void slotConfigChanged())
};
}
#endif
@@ -0,0 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<author>Zack Rusin &lt;zack@kde.org&gt;</author>
<comment>Licensed under GNU LGPL</comment>
<class>SonnetConfigUI</class>
<widget class="QWidget" name="SonnetConfigUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>700</width>
<height>833</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="nobackendfound">
<property name="font">
<font>
<weight>75</weight>
<italic>true</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>No backend like hunspell/aspell/myspell installed</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="textLabel1">
<property name="text">
<string>Default language:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Sonnet::DictionaryComboBox" name="m_langCombo">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox1">
<property name="title">
<string>Preferred Languages</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
<layout class="QVBoxLayout">
<item row="0">
<widget class="QListWidget" name="languageList">
<property name="toolTip">
<string>Choose your preferred languages</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox2">
<property name="title">
<string>Options</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
<layout class="QGridLayout">
<item row="2" column="0">
<widget class="QCheckBox" name="kcfg_autodetectLanguage">
<property name="text">
<string>Enable autodetection of &amp;language</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="kcfg_backgroundCheckerEnabled">
<property name="text">
<string>Enable &amp;background spellchecking</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="kcfg_checkerEnabledByDefault">
<property name="text">
<string>&amp;Automatic spell checking enabled by default</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="kcfg_skipUppercase">
<property name="text">
<string>Skip all &amp;uppercase words</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="kcfg_skipRunTogether">
<property name="text">
<string>S&amp;kip run-together words</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Ignored Words</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="ignoredWordsLayout">
<item row="1" column="0">
<widget class="QListWidget" name="ignoreListWidget"/>
</item>
<item row="0" column="0">
<widget class="QLineEdit" name="newIgnoreEdit"/>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="addButton">
<property name="text">
<string>&amp;Add</string>
</property>
<property name="icon">
<iconset theme="list-add"/>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QVBoxLayout" name="removeButtonLayout">
<item>
<widget class="QPushButton" name="removeButton">
<property name="text">
<string>&amp;Remove</string>
</property>
<property name="icon">
<iconset theme="list-remove"/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Sonnet::DictionaryComboBox</class>
<extends>QComboBox</extends>
<header>sonnet/dictionarycombobox.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,202 @@
/*
* configwidget.cpp
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "configview.h"
#include "ui_configui.h"
#include "ui_debug.h"
#include <QCheckBox>
#include <QLineEdit>
#include <QListWidget>
using namespace Sonnet;
class Sonnet::ConfigViewPrivate
{
public:
explicit ConfigViewPrivate(ConfigView *v);
Ui_SonnetConfigUI ui;
QWidget *wdg = nullptr;
QStringList ignoreList;
ConfigView *q;
void slotUpdateButton(const QString &text);
void slotSelectionChanged();
void slotIgnoreWordAdded();
void slotIgnoreWordRemoved();
};
ConfigViewPrivate::ConfigViewPrivate(ConfigView *v)
{
q = v;
}
void ConfigViewPrivate::slotUpdateButton(const QString &text)
{
ui.addButton->setEnabled(!text.isEmpty());
}
void ConfigViewPrivate::slotSelectionChanged()
{
ui.removeButton->setEnabled(!ui.ignoreListWidget->selectedItems().isEmpty());
}
void ConfigViewPrivate::slotIgnoreWordAdded()
{
QString newWord = ui.newIgnoreEdit->text();
ui.newIgnoreEdit->clear();
if (newWord.isEmpty() || ignoreList.contains(newWord)) {
return;
}
ignoreList.append(newWord);
ui.ignoreListWidget->clear();
ui.ignoreListWidget->addItems(ignoreList);
Q_EMIT q->configChanged();
}
void ConfigViewPrivate::slotIgnoreWordRemoved()
{
const QList<QListWidgetItem *> selectedItems = ui.ignoreListWidget->selectedItems();
for (const QListWidgetItem *item : selectedItems) {
ignoreList.removeAll(item->text());
}
ui.ignoreListWidget->clear();
ui.ignoreListWidget->addItems(ignoreList);
Q_EMIT q->configChanged();
}
ConfigView::ConfigView(QWidget *parent)
: QWidget(parent)
, d(new ConfigViewPrivate(this))
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setObjectName(QStringLiteral("SonnetConfigUILayout"));
d->wdg = new QWidget(this);
d->ui.setupUi(d->wdg);
d->ui.languageList->setProperty("_breeze_force_frame", true);
for (int i = 0; i < d->ui.m_langCombo->count(); i++) {
const QString tag = d->ui.m_langCombo->itemData(i).toString();
if (tag.isEmpty()) { // skip separator
continue;
}
auto *item = new QListWidgetItem(d->ui.m_langCombo->itemText(i), d->ui.languageList);
item->setData(Qt::UserRole, tag);
}
d->ui.kcfg_backgroundCheckerEnabled->hide(); // hidden by default
connect(d->ui.addButton, &QAbstractButton::clicked, this, [this] {
d->slotIgnoreWordAdded();
});
connect(d->ui.removeButton, &QAbstractButton::clicked, this, [this] {
d->slotIgnoreWordRemoved();
});
layout->addWidget(d->wdg);
connect(d->ui.newIgnoreEdit, &QLineEdit::textChanged, this, [this](const QString &text) {
d->slotUpdateButton(text);
});
connect(d->ui.ignoreListWidget, &QListWidget::itemSelectionChanged, this, [this] {
d->slotSelectionChanged();
});
d->ui.addButton->setEnabled(false);
d->ui.removeButton->setEnabled(false);
connect(d->ui.m_langCombo, &DictionaryComboBox::dictionaryChanged, this, &ConfigView::configChanged);
connect(d->ui.languageList, &QListWidget::itemChanged, this, &ConfigView::configChanged);
connect(d->ui.kcfg_backgroundCheckerEnabled, &QAbstractButton::clicked, this, &ConfigView::configChanged);
connect(d->ui.kcfg_skipUppercase, &QAbstractButton::clicked, this, &ConfigView::configChanged);
connect(d->ui.kcfg_skipRunTogether, &QAbstractButton::clicked, this, &ConfigView::configChanged);
connect(d->ui.kcfg_checkerEnabledByDefault, &QAbstractButton::clicked, this, &ConfigView::configChanged);
connect(d->ui.kcfg_autodetectLanguage, &QAbstractButton::clicked, this, &ConfigView::configChanged);
}
ConfigView::~ConfigView() = default;
void ConfigView::setNoBackendFoundVisible(bool show)
{
d->ui.nobackendfound->setVisible(show);
}
bool ConfigView::noBackendFoundVisible() const
{
return d->ui.nobackendfound->isVisible();
}
void ConfigView::setBackgroundCheckingButtonShown(bool b)
{
d->ui.kcfg_backgroundCheckerEnabled->setVisible(b);
}
bool ConfigView::backgroundCheckingButtonShown() const
{
return !d->ui.kcfg_backgroundCheckerEnabled->isHidden();
}
void ConfigView::setLanguage(const QString &language)
{
d->ui.m_langCombo->setCurrentByDictionary(language);
}
QString ConfigView::language() const
{
if (d->ui.m_langCombo->count()) {
return d->ui.m_langCombo->currentDictionary();
} else {
return QString();
}
}
void ConfigView::setPreferredLanguages(const QStringList &preferredLanguages)
{
for (int i = 0; i < d->ui.languageList->count(); ++i) {
QListWidgetItem *item = d->ui.languageList->item(i);
QString tag = item->data(Qt::UserRole).toString();
if (preferredLanguages.contains(tag)) {
item->setCheckState(Qt::Checked);
} else {
item->setCheckState(Qt::Unchecked);
}
}
Q_EMIT configChanged();
}
QStringList ConfigView::preferredLanguages() const
{
QStringList preferredLanguages;
for (int i = 0; i < d->ui.languageList->count(); i++) {
if (d->ui.languageList->item(i)->checkState() == Qt::Unchecked) {
continue;
}
preferredLanguages << d->ui.languageList->item(i)->data(Qt::UserRole).toString();
}
return preferredLanguages;
}
void ConfigView::setIgnoreList(const QStringList &ignoreList)
{
d->ignoreList = ignoreList;
d->ignoreList.sort();
d->ui.ignoreListWidget->clear();
d->ui.ignoreListWidget->addItems(d->ignoreList);
Q_EMIT configChanged();
}
QStringList ConfigView::ignoreList() const
{
return d->ignoreList;
}
#include "moc_configview.cpp"
@@ -0,0 +1,54 @@
/*
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_CONFIGVIEW_H
#define SONNET_CONFIGVIEW_H
#include <QWidget>
#include "sonnetui_export.h"
#include <memory>
namespace Sonnet
{
class ConfigViewPrivate;
class SONNETUI_EXPORT ConfigView : public QWidget
{
Q_OBJECT
Q_PROPERTY(QString language READ language WRITE setLanguage)
Q_PROPERTY(QStringList ignoreList READ ignoreList WRITE setIgnoreList)
Q_PROPERTY(QStringList preferredLanguages READ preferredLanguages WRITE setPreferredLanguages)
Q_PROPERTY(bool backgroundCheckingButtonShown READ backgroundCheckingButtonShown WRITE setBackgroundCheckingButtonShown)
Q_PROPERTY(bool showNoBackendFound READ noBackendFoundVisible WRITE setNoBackendFoundVisible)
public:
explicit ConfigView(QWidget *parent = nullptr);
~ConfigView() override;
bool backgroundCheckingButtonShown() const;
bool noBackendFoundVisible() const;
QStringList preferredLanguages() const;
QString language() const;
QStringList ignoreList() const;
public Q_SLOTS:
void setNoBackendFoundVisible(bool show);
void setBackgroundCheckingButtonShown(bool);
void setPreferredLanguages(const QStringList &ignoreList);
void setLanguage(const QString &language);
void setIgnoreList(const QStringList &ignoreList);
Q_SIGNALS:
void configChanged();
private:
std::unique_ptr<ConfigViewPrivate> const d;
};
}
#endif
@@ -0,0 +1,200 @@
/*
* configwidget.cpp
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "configwidget.h"
#include "ui_configui.h"
#include "loader_p.h"
#include "settings.h"
#include "settingsimpl_p.h"
#include "ui_debug.h"
#include <QCheckBox>
#include <QLineEdit>
#include <QListWidget>
#include <QListWidgetItem>
using namespace Sonnet;
class Sonnet::ConfigWidgetPrivate
{
public:
Ui_SonnetConfigUI ui;
Settings *settings = nullptr;
QWidget *wdg = nullptr;
};
ConfigWidget::ConfigWidget(QWidget *parent)
: QWidget(parent)
, d(new ConfigWidgetPrivate)
{
d->settings = new Settings(this);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setObjectName(QStringLiteral("SonnetConfigUILayout"));
d->wdg = new QWidget(this);
d->ui.setupUi(d->wdg);
d->ui.languageList->setProperty("_breeze_force_frame", true);
d->ui.m_langCombo->setCurrentByDictionary(d->settings->defaultLanguage());
QStringList preferredLanguages = d->settings->preferredLanguages();
for (int i = 0; i < d->ui.m_langCombo->count(); i++) {
const QString tag = d->ui.m_langCombo->itemData(i).toString();
if (tag.isEmpty()) { // skip separator
continue;
}
QListWidgetItem *item = new QListWidgetItem(d->ui.m_langCombo->itemText(i), d->ui.languageList);
item->setData(Qt::UserRole, tag);
if (preferredLanguages.contains(tag)) {
item->setCheckState(Qt::Checked);
} else {
item->setCheckState(Qt::Unchecked);
}
}
d->ui.kcfg_skipUppercase->setChecked(d->settings->skipUppercase());
d->ui.kcfg_skipRunTogether->setChecked(d->settings->skipRunTogether());
d->ui.kcfg_checkerEnabledByDefault->setChecked(d->settings->checkerEnabledByDefault());
d->ui.kcfg_autodetectLanguage->setChecked(d->settings->autodetectLanguage());
QStringList ignoreList = d->settings->currentIgnoreList();
ignoreList.sort();
d->ui.ignoreListWidget->addItems(ignoreList);
d->ui.kcfg_backgroundCheckerEnabled->setChecked(d->settings->backgroundCheckerEnabled());
d->ui.kcfg_backgroundCheckerEnabled->hide(); // hidden by default
connect(d->ui.addButton, &QAbstractButton::clicked, this, &ConfigWidget::slotIgnoreWordAdded);
connect(d->ui.removeButton, &QAbstractButton::clicked, this, &ConfigWidget::slotIgnoreWordRemoved);
layout->addWidget(d->wdg);
connect(d->ui.m_langCombo, &DictionaryComboBox::dictionaryChanged, this, &ConfigWidget::configChanged);
connect(d->ui.languageList, &QListWidget::itemChanged, this, &ConfigWidget::configChanged);
connect(d->ui.kcfg_backgroundCheckerEnabled, &QAbstractButton::clicked, this, &ConfigWidget::configChanged);
connect(d->ui.kcfg_skipUppercase, &QAbstractButton::clicked, this, &ConfigWidget::configChanged);
connect(d->ui.kcfg_skipRunTogether, &QAbstractButton::clicked, this, &ConfigWidget::configChanged);
connect(d->ui.kcfg_checkerEnabledByDefault, &QAbstractButton::clicked, this, &ConfigWidget::configChanged);
connect(d->ui.kcfg_autodetectLanguage, &QAbstractButton::clicked, this, &ConfigWidget::configChanged);
connect(d->ui.newIgnoreEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotUpdateButton);
connect(d->ui.ignoreListWidget, &QListWidget::itemSelectionChanged, this, &ConfigWidget::slotSelectionChanged);
d->ui.nobackendfound->setVisible(d->settings->clients().isEmpty());
d->ui.addButton->setEnabled(false);
d->ui.removeButton->setEnabled(false);
}
ConfigWidget::~ConfigWidget() = default;
void ConfigWidget::slotUpdateButton(const QString &text)
{
d->ui.addButton->setEnabled(!text.isEmpty());
}
void ConfigWidget::slotSelectionChanged()
{
d->ui.removeButton->setEnabled(!d->ui.ignoreListWidget->selectedItems().isEmpty());
}
void ConfigWidget::save()
{
setFromGui();
}
void ConfigWidget::setFromGui()
{
if (d->ui.m_langCombo->count()) {
d->settings->setDefaultLanguage(d->ui.m_langCombo->currentDictionary());
}
QStringList preferredLanguages;
for (int i = 0; i < d->ui.languageList->count(); i++) {
if (d->ui.languageList->item(i)->checkState() == Qt::Unchecked) {
continue;
}
preferredLanguages << d->ui.languageList->item(i)->data(Qt::UserRole).toString();
}
d->settings->setPreferredLanguages(preferredLanguages);
d->settings->setSkipUppercase(d->ui.kcfg_skipUppercase->isChecked());
d->settings->setSkipRunTogether(d->ui.kcfg_skipRunTogether->isChecked());
d->settings->setBackgroundCheckerEnabled(d->ui.kcfg_backgroundCheckerEnabled->isChecked());
d->settings->setCheckerEnabledByDefault(d->ui.kcfg_checkerEnabledByDefault->isChecked());
d->settings->setAutodetectLanguage(d->ui.kcfg_autodetectLanguage->isChecked());
if (d->settings->modified()) {
d->settings->save();
}
}
void ConfigWidget::slotIgnoreWordAdded()
{
QStringList ignoreList = d->settings->currentIgnoreList();
QString newWord = d->ui.newIgnoreEdit->text();
d->ui.newIgnoreEdit->clear();
if (newWord.isEmpty() || ignoreList.contains(newWord)) {
return;
}
ignoreList.append(newWord);
d->settings->setCurrentIgnoreList(ignoreList);
d->ui.ignoreListWidget->clear();
d->ui.ignoreListWidget->addItems(ignoreList);
Q_EMIT configChanged();
}
void ConfigWidget::slotIgnoreWordRemoved()
{
QStringList ignoreList = d->settings->currentIgnoreList();
const QList<QListWidgetItem *> selectedItems = d->ui.ignoreListWidget->selectedItems();
for (const QListWidgetItem *item : selectedItems) {
ignoreList.removeAll(item->text());
}
d->settings->setCurrentIgnoreList(ignoreList);
d->ui.ignoreListWidget->clear();
d->ui.ignoreListWidget->addItems(ignoreList);
Q_EMIT configChanged();
}
void ConfigWidget::setBackgroundCheckingButtonShown(bool b)
{
d->ui.kcfg_backgroundCheckerEnabled->setVisible(b);
}
bool ConfigWidget::backgroundCheckingButtonShown() const
{
return !d->ui.kcfg_backgroundCheckerEnabled->isHidden();
}
void ConfigWidget::slotDefault()
{
d->ui.kcfg_autodetectLanguage->setChecked(Settings::defaultAutodetectLanguage());
d->ui.kcfg_skipUppercase->setChecked(Settings::defaultSkipUppercase());
d->ui.kcfg_skipRunTogether->setChecked(Settings::defauktSkipRunTogether());
d->ui.kcfg_checkerEnabledByDefault->setChecked(Settings::defaultCheckerEnabledByDefault());
d->ui.kcfg_backgroundCheckerEnabled->setChecked(Settings::defaultBackgroundCheckerEnabled());
d->ui.ignoreListWidget->clear();
d->ui.m_langCombo->setCurrentByDictionary(d->settings->defaultLanguage());
}
void ConfigWidget::setLanguage(const QString &language)
{
d->ui.m_langCombo->setCurrentByDictionary(language);
}
QString ConfigWidget::language() const
{
if (d->ui.m_langCombo->count()) {
return d->ui.m_langCombo->currentDictionary();
} else {
return QString();
}
}
#include "moc_configwidget.cpp"
@@ -0,0 +1,73 @@
/*
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_CONFIGWIDGET_H
#define SONNET_CONFIGWIDGET_H
#include "sonnetui_export.h"
#include <QWidget>
#include <memory>
namespace Sonnet
{
class ConfigWidgetPrivate;
/// The sonnet ConfigWidget
class SONNETUI_EXPORT ConfigWidget : public QWidget
{
Q_OBJECT
public:
explicit ConfigWidget(QWidget *parent);
~ConfigWidget() override;
bool backgroundCheckingButtonShown() const;
/**
* Sets the language/dictionary that will be selected by default
* in this config widget.
* This overrides the setting in the config file.
*
* @param language the language which will be selected by default.
* @since 4.1
*/
void setLanguage(const QString &language);
/**
* Get the currently selected language for spell checking. Returns an empty string if
* Sonnet was built without any spellchecking plugins.
* @return the language currently selected in the language combobox
* @since 4.1
*/
QString language() const;
public Q_SLOTS:
void save();
void setBackgroundCheckingButtonShown(bool);
void slotDefault();
protected Q_SLOTS:
void slotIgnoreWordRemoved();
void slotIgnoreWordAdded();
private Q_SLOTS:
SONNETUI_NO_EXPORT void slotUpdateButton(const QString &text);
SONNETUI_NO_EXPORT void slotSelectionChanged();
Q_SIGNALS:
/**
* Signal sends when config was changed
* @since 4.1
*/
void configChanged();
private:
SONNETUI_NO_EXPORT void setFromGui();
private:
std::unique_ptr<ConfigWidgetPrivate> const d;
};
}
#endif
@@ -0,0 +1,18 @@
include(ECMAddQtDesignerPlugin)
ecm_qtdesignerplugin_widget(Sonnet::DictionaryComboBox
TOOLTIP "Dictionary Combobox"
WHATSTHIS "A combobox to select a dictionary for spellchecking"
GROUP "Sonnet (KF6)"
)
ecm_add_qtdesignerplugin(sonnetuiwidgets
NAME SonnetUiWidgets
OUTPUT_NAME sonnet6widgets
WIDGETS
Sonnet::DictionaryComboBox
LINK_LIBRARIES
KF6::SonnetUi
INSTALL_DESTINATION "${KDE_INSTALL_QTPLUGINDIR}/designer"
COMPONENT Devel
)
@@ -0,0 +1,405 @@
/*
* dialog.cpp
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "dialog.h"
#include "ui_sonnetui.h"
#include "backgroundchecker.h"
#include "settingsimpl_p.h"
#include "speller.h"
#include <QProgressDialog>
#include <QDialogButtonBox>
#include <QMessageBox>
#include <QPushButton>
#include <QStringListModel>
namespace Sonnet
{
// to initially disable sorting in the suggestions listview
#define NONSORTINGCOLUMN 2
class ReadOnlyStringListModel : public QStringListModel
{
public:
explicit ReadOnlyStringListModel(QObject *parent)
: QStringListModel(parent)
{
}
Qt::ItemFlags flags(const QModelIndex &index) const override
{
Q_UNUSED(index);
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
};
class DialogPrivate
{
public:
Ui_SonnetUi ui;
ReadOnlyStringListModel *suggestionsModel = nullptr;
QWidget *wdg = nullptr;
QDialogButtonBox *buttonBox = nullptr;
QProgressDialog *progressDialog = nullptr;
QString originalBuffer;
BackgroundChecker *checker = nullptr;
QString currentWord;
int currentPosition;
QMap<QString, QString> replaceAllMap;
bool restart; // used when text is distributed across several qtextedits, eg in KAider
QMap<QString, QString> dictsMap;
int progressDialogTimeout;
bool showCompletionMessageBox;
bool spellCheckContinuedAfterReplacement;
bool canceled;
void deleteProgressDialog(bool directly)
{
if (progressDialog) {
progressDialog->hide();
if (directly) {
delete progressDialog;
} else {
progressDialog->deleteLater();
}
progressDialog = nullptr;
}
}
};
Dialog::Dialog(BackgroundChecker *checker, QWidget *parent)
: QDialog(parent)
, d(new DialogPrivate)
{
setModal(true);
setWindowTitle(tr("Check Spelling", "@title:window"));
d->checker = checker;
d->canceled = false;
d->showCompletionMessageBox = false;
d->spellCheckContinuedAfterReplacement = true;
d->progressDialogTimeout = -1;
d->progressDialog = nullptr;
initGui();
initConnections();
}
Dialog::~Dialog() = default;
void Dialog::initConnections()
{
connect(d->ui.m_addBtn, &QAbstractButton::clicked, this, &Dialog::slotAddWord);
connect(d->ui.m_replaceBtn, &QAbstractButton::clicked, this, &Dialog::slotReplaceWord);
connect(d->ui.m_replaceAllBtn, &QAbstractButton::clicked, this, &Dialog::slotReplaceAll);
connect(d->ui.m_skipBtn, &QAbstractButton::clicked, this, &Dialog::slotSkip);
connect(d->ui.m_skipAllBtn, &QAbstractButton::clicked, this, &Dialog::slotSkipAll);
connect(d->ui.m_suggestBtn, &QAbstractButton::clicked, this, &Dialog::slotSuggest);
connect(d->ui.m_language, &DictionaryComboBox::textActivated, this, &Dialog::slotChangeLanguage);
connect(d->ui.m_suggestions, &QListView::clicked, this, &Dialog::slotSelectionChanged);
connect(d->checker, &BackgroundChecker::misspelling, this, &Dialog::slotMisspelling);
connect(d->checker, &BackgroundChecker::done, this, &Dialog::slotDone);
connect(d->ui.m_suggestions, &QListView::doubleClicked, this, [this](const QModelIndex &) {
slotReplaceWord();
});
connect(d->buttonBox, &QDialogButtonBox::accepted, this, &Dialog::slotFinished);
connect(d->buttonBox, &QDialogButtonBox::rejected, this, &Dialog::slotCancel);
connect(d->ui.m_replacement, &QLineEdit::returnPressed, this, &Dialog::slotReplaceWord);
connect(d->ui.m_autoCorrect, &QPushButton::clicked, this, &Dialog::slotAutocorrect);
// button use by kword/kpresenter
// hide by default
d->ui.m_autoCorrect->hide();
}
void Dialog::initGui()
{
QVBoxLayout *layout = new QVBoxLayout(this);
d->wdg = new QWidget(this);
d->ui.setupUi(d->wdg);
layout->addWidget(d->wdg);
setGuiEnabled(false);
d->buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(d->wdg);
layout->addWidget(d->buttonBox);
// d->ui.m_suggestions->setSorting( NONSORTINGCOLUMN );
fillDictionaryComboBox();
d->restart = false;
d->suggestionsModel = new ReadOnlyStringListModel(this);
d->ui.m_suggestions->setModel(d->suggestionsModel);
}
void Dialog::activeAutoCorrect(bool _active)
{
if (_active) {
d->ui.m_autoCorrect->show();
} else {
d->ui.m_autoCorrect->hide();
}
}
void Dialog::showProgressDialog(int timeout)
{
d->progressDialogTimeout = timeout;
}
void Dialog::showSpellCheckCompletionMessage(bool b)
{
d->showCompletionMessageBox = b;
}
void Dialog::setSpellCheckContinuedAfterReplacement(bool b)
{
d->spellCheckContinuedAfterReplacement = b;
}
void Dialog::slotAutocorrect()
{
setGuiEnabled(false);
setProgressDialogVisible(true);
Q_EMIT autoCorrect(d->currentWord, d->ui.m_replacement->text());
slotReplaceWord();
}
void Dialog::setGuiEnabled(bool b)
{
d->wdg->setEnabled(b);
}
void Dialog::setProgressDialogVisible(bool b)
{
if (!b) {
d->deleteProgressDialog(true);
} else if (d->progressDialogTimeout >= 0) {
if (d->progressDialog) {
return;
}
d->progressDialog = new QProgressDialog(this);
d->progressDialog->setLabelText(tr("Spell checking in progress…", "@info:progress"));
d->progressDialog->setWindowTitle(tr("Check Spelling", "@title:window"));
d->progressDialog->setModal(true);
d->progressDialog->setAutoClose(false);
d->progressDialog->setAutoReset(false);
// create an 'indefinite' progress box as we currently cannot get progress feedback from
// the speller
d->progressDialog->reset();
d->progressDialog->setRange(0, 0);
d->progressDialog->setValue(0);
connect(d->progressDialog, &QProgressDialog::canceled, this, &Dialog::slotCancel);
d->progressDialog->setMinimumDuration(d->progressDialogTimeout);
}
}
void Dialog::slotFinished()
{
setProgressDialogVisible(false);
Q_EMIT stop();
// FIXME: should we emit done here?
Q_EMIT spellCheckDone(d->checker->text());
Q_EMIT spellCheckStatus(tr("Spell check stopped."));
accept();
}
void Dialog::slotCancel()
{
d->canceled = true;
d->deleteProgressDialog(false); // this method can be called in response to
// pressing 'Cancel' on the dialog
Q_EMIT cancel();
Q_EMIT spellCheckStatus(tr("Spell check canceled."));
reject();
}
QString Dialog::originalBuffer() const
{
return d->originalBuffer;
}
QString Dialog::buffer() const
{
return d->checker->text();
}
void Dialog::setBuffer(const QString &buf)
{
d->originalBuffer = buf;
// it is possible to change buffer inside slot connected to done() signal
d->restart = true;
}
void Dialog::fillDictionaryComboBox()
{
// Since m_language is changed to DictionaryComboBox most code here is gone,
// So fillDictionaryComboBox() could be removed and code moved to initGui()
// because the call in show() looks obsolete
Speller speller = d->checker->speller();
d->dictsMap = speller.availableDictionaries();
updateDictionaryComboBox();
}
void Dialog::updateDictionaryComboBox()
{
const Speller &speller = d->checker->speller();
d->ui.m_language->setCurrentByDictionary(speller.language());
}
void Dialog::updateDialog(const QString &word)
{
d->ui.m_unknownWord->setText(word);
d->ui.m_contextLabel->setText(d->checker->currentContext());
const QStringList suggs = d->checker->suggest(word);
if (suggs.isEmpty()) {
d->ui.m_replacement->clear();
} else {
d->ui.m_replacement->setText(suggs.first());
}
fillSuggestions(suggs);
}
void Dialog::show()
{
d->canceled = false;
fillDictionaryComboBox();
if (d->originalBuffer.isEmpty()) {
d->checker->start();
} else {
d->checker->setText(d->originalBuffer);
}
setProgressDialogVisible(true);
}
void Dialog::slotAddWord()
{
setGuiEnabled(false);
setProgressDialogVisible(true);
d->checker->addWordToPersonal(d->currentWord);
d->checker->continueChecking();
}
void Dialog::slotReplaceWord()
{
setGuiEnabled(false);
setProgressDialogVisible(true);
QString replacementText = d->ui.m_replacement->text();
Q_EMIT replace(d->currentWord, d->currentPosition, replacementText);
if (d->spellCheckContinuedAfterReplacement) {
d->checker->replace(d->currentPosition, d->currentWord, replacementText);
d->checker->continueChecking();
} else {
d->checker->stop();
}
}
void Dialog::slotReplaceAll()
{
setGuiEnabled(false);
setProgressDialogVisible(true);
d->replaceAllMap.insert(d->currentWord, d->ui.m_replacement->text());
slotReplaceWord();
}
void Dialog::slotSkip()
{
setGuiEnabled(false);
setProgressDialogVisible(true);
d->checker->continueChecking();
}
void Dialog::slotSkipAll()
{
setGuiEnabled(false);
setProgressDialogVisible(true);
//### do we want that or should we have a d->ignoreAll list?
Speller speller = d->checker->speller();
speller.addToPersonal(d->currentWord);
d->checker->setSpeller(speller);
d->checker->continueChecking();
}
void Dialog::slotSuggest()
{
const QStringList suggs = d->checker->suggest(d->ui.m_replacement->text());
fillSuggestions(suggs);
}
void Dialog::slotChangeLanguage(const QString &lang)
{
const QString languageCode = d->dictsMap[lang];
if (!languageCode.isEmpty()) {
d->checker->changeLanguage(languageCode);
slotSuggest();
Q_EMIT languageChanged(languageCode);
}
}
void Dialog::slotSelectionChanged(const QModelIndex &item)
{
d->ui.m_replacement->setText(item.data().toString());
}
void Dialog::fillSuggestions(const QStringList &suggs)
{
d->suggestionsModel->setStringList(suggs);
}
void Dialog::slotMisspelling(const QString &word, int start)
{
setGuiEnabled(true);
setProgressDialogVisible(false);
Q_EMIT misspelling(word, start);
// NOTE this is HACK I had to introduce because BackgroundChecker lacks 'virtual' marks on methods
// this dramatically reduces spellchecking time in Lokalize
// as this doesn't fetch suggestions for words that are present in msgid
if (!updatesEnabled()) {
return;
}
d->currentWord = word;
d->currentPosition = start;
if (d->replaceAllMap.contains(word)) {
d->ui.m_replacement->setText(d->replaceAllMap[word]);
slotReplaceWord();
} else {
updateDialog(word);
}
QDialog::show();
}
void Dialog::slotDone()
{
d->restart = false;
Q_EMIT spellCheckDone(d->checker->text());
if (d->restart) {
updateDictionaryComboBox();
d->checker->setText(d->originalBuffer);
d->restart = false;
} else {
setProgressDialogVisible(false);
Q_EMIT spellCheckStatus(tr("Spell check complete."));
accept();
if (!d->canceled && d->showCompletionMessageBox) {
QMessageBox::information(this, tr("Spell check complete."), tr("Check Spelling", "@title:window"));
}
}
}
}
#include "moc_dialog.cpp"
@@ -0,0 +1,153 @@
/*
* dialog.h
*
* SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_DIALOG_H
#define SONNET_DIALOG_H
#include "sonnetui_export.h"
#include <QDialog>
#include <memory>
class QModelIndex;
namespace Sonnet
{
class BackgroundChecker;
class DialogPrivate;
/**
* @class Sonnet::Dialog dialog.h <Sonnet/Dialog>
*
* @short Spellcheck dialog
*
* \code
* Sonnet::Dialog dlg = new Sonnet::Dialog(
* new Sonnet::BackgroundChecker(this), this);
* //connect signals
* ...
* dlg->setBuffer( someText );
* dlg->show();
* \endcode
*
* You can change buffer inside a slot connected to done() signal
* and spellcheck will continue with new data automatically.
*/
class SONNETUI_EXPORT Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(BackgroundChecker *checker, QWidget *parent);
~Dialog() override;
QString originalBuffer() const;
QString buffer() const;
void show();
void activeAutoCorrect(bool _active);
// Hide warning about done(), which is a slot in QDialog and a signal here.
using QDialog::done;
/**
* Controls whether an (indefinite) progress dialog is shown when the spell
* checking takes longer than the given time to complete. By default no
* progress dialog is shown. If the progress dialog is set to be shown, no
* time consuming operation (for example, showing a notification message) should
* be performed in a slot connected to the 'done' signal as this might trigger
* the progress dialog unnecessarily.
*
* @param timeout time after which the progress dialog should appear; a negative
* value can be used to hide it
* @since 4.4
*/
void showProgressDialog(int timeout = 500);
/**
* Controls whether a message box indicating the completion of the spell checking
* is shown or not. By default it is not shown.
*
* @since 4.4
*/
void showSpellCheckCompletionMessage(bool b = true);
/**
* Controls whether the spell checking is continued after the replacement of a
* misspelled word has been performed. By default it is continued.
*
* @since 4.4
*/
void setSpellCheckContinuedAfterReplacement(bool b);
public Q_SLOTS:
void setBuffer(const QString &);
Q_SIGNALS:
/**
* The dialog won't be closed if you setBuffer() in slot connected to this signal
* Also emitted after stop() signal
* @Since 5.65
*/
void spellCheckDone(const QString &newBuffer);
void misspelling(const QString &word, int start);
void replace(const QString &oldWord, int start, const QString &newWord);
void stop();
void cancel();
void autoCorrect(const QString &currentWord, const QString &replaceWord);
/**
* Signal sends when spell checking is finished/stopped/completed
* @since 4.1
*/
void spellCheckStatus(const QString &);
/**
* Emitted when the user changes the language used for spellchecking,
* which is shown in a combobox of this dialog.
*
* @param dictionary the new language the user selected
* @since 4.1
*/
void languageChanged(const QString &language);
private Q_SLOTS:
SONNETUI_NO_EXPORT void slotMisspelling(const QString &word, int start);
SONNETUI_NO_EXPORT void slotDone();
SONNETUI_NO_EXPORT void slotFinished();
SONNETUI_NO_EXPORT void slotCancel();
SONNETUI_NO_EXPORT void slotAddWord();
SONNETUI_NO_EXPORT void slotReplaceWord();
SONNETUI_NO_EXPORT void slotReplaceAll();
SONNETUI_NO_EXPORT void slotSkip();
SONNETUI_NO_EXPORT void slotSkipAll();
SONNETUI_NO_EXPORT void slotSuggest();
SONNETUI_NO_EXPORT void slotChangeLanguage(const QString &);
SONNETUI_NO_EXPORT void slotSelectionChanged(const QModelIndex &);
SONNETUI_NO_EXPORT void slotAutocorrect();
SONNETUI_NO_EXPORT void setGuiEnabled(bool b);
SONNETUI_NO_EXPORT void setProgressDialogVisible(bool b);
private:
SONNETUI_NO_EXPORT void updateDialog(const QString &word);
SONNETUI_NO_EXPORT void fillDictionaryComboBox();
SONNETUI_NO_EXPORT void updateDictionaryComboBox();
SONNETUI_NO_EXPORT void fillSuggestions(const QStringList &suggs);
SONNETUI_NO_EXPORT void initConnections();
SONNETUI_NO_EXPORT void initGui();
SONNETUI_NO_EXPORT void continueChecking();
private:
DialogPrivate *const d;
Q_DISABLE_COPY(Dialog)
};
}
#endif
@@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: 2003 Ingo Kloecker <kloecker@kde.org>
* SPDX-FileCopyrightText: 2008 Tom Albers <tomalbers@kde.nl>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "dictionarycombobox.h"
#include "ui_debug.h"
#include <speller.h>
namespace Sonnet
{
//@cond PRIVATE
class DictionaryComboBoxPrivate
{
public:
explicit DictionaryComboBoxPrivate(DictionaryComboBox *combo)
: q(combo)
{
}
DictionaryComboBox *const q;
void slotDictionaryChanged(int idx);
};
void DictionaryComboBoxPrivate::slotDictionaryChanged(int idx)
{
Q_EMIT q->dictionaryChanged(q->itemData(idx).toString());
Q_EMIT q->dictionaryNameChanged(q->itemText(idx));
}
//@endcon
DictionaryComboBox::DictionaryComboBox(QWidget *parent)
: QComboBox(parent)
, d(new DictionaryComboBoxPrivate(this))
{
reloadCombo();
connect(this, SIGNAL(activated(int)), SLOT(slotDictionaryChanged(int)));
}
DictionaryComboBox::~DictionaryComboBox() = default;
QString DictionaryComboBox::currentDictionaryName() const
{
return currentText();
}
QString DictionaryComboBox::currentDictionary() const
{
return itemData(currentIndex()).toString();
}
bool DictionaryComboBox::assignDictionnaryName(const QString &name)
{
if (name.isEmpty() || name == currentText()) {
return false;
}
int idx = findText(name);
if (idx == -1) {
qCDebug(SONNET_LOG_UI) << "name not found" << name;
return false;
}
setCurrentIndex(idx);
d->slotDictionaryChanged(idx);
return true;
}
void DictionaryComboBox::setCurrentByDictionaryName(const QString &name)
{
assignDictionnaryName(name);
}
bool DictionaryComboBox::assignByDictionnary(const QString &dictionary)
{
if (dictionary.isEmpty()) {
return false;
}
if (dictionary == itemData(currentIndex()).toString()) {
return true;
}
int idx = findData(dictionary);
if (idx == -1) {
qCDebug(SONNET_LOG_UI) << "dictionary not found" << dictionary;
return false;
}
setCurrentIndex(idx);
d->slotDictionaryChanged(idx);
return true;
}
void DictionaryComboBox::setCurrentByDictionary(const QString &dictionary)
{
assignByDictionnary(dictionary);
}
void DictionaryComboBox::reloadCombo()
{
clear();
Sonnet::Speller speller;
QMap<QString, QString> preferredDictionaries = speller.preferredDictionaries();
QMapIterator<QString, QString> i(preferredDictionaries);
while (i.hasNext()) {
i.next();
addItem(i.key(), i.value());
}
if (count()) {
insertSeparator(count());
}
QMap<QString, QString> dictionaries = speller.availableDictionaries();
i = dictionaries;
while (i.hasNext()) {
i.next();
if (preferredDictionaries.contains(i.key())) {
continue;
}
addItem(i.key(), i.value());
}
}
} // namespace Sonnet
#include "moc_dictionarycombobox.cpp"
@@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2003 Ingo Kloecker <kloecker@kde.org>
* SPDX-FileCopyrightText: 2008 Tom Albers <tomalbers@kde.nl>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_DICTIONARYCOMBOBOX_H
#define SONNET_DICTIONARYCOMBOBOX_H
#include "sonnetui_export.h"
#include <QComboBox>
#include <memory>
namespace Sonnet
{
class DictionaryComboBoxPrivate;
/**
* @class Sonnet::DictionaryComboBox dictionarycombobox.h <Sonnet/DictionaryComboBox>
*
* @short A combo box for selecting the dictionary used for spell checking.
* @author Ingo Kloecker <kloecker@kde.org>
* @author Tom Albers <tomalbers@kde.nl>
* @since 4.2
**/
class SONNETUI_EXPORT DictionaryComboBox : public QComboBox
{
Q_OBJECT
public:
/**
* Constructor
*/
explicit DictionaryComboBox(QWidget *parent = nullptr);
/**
* Destructor
*/
~DictionaryComboBox() override;
/**
* Clears the widget and reloads the dictionaries from Sonnet.
* Remember to set the dictionary you want selected after calling this function.
*/
void reloadCombo();
/**
* Returns the current dictionary name, for example "German (Switzerland)"
*/
QString currentDictionaryName() const;
/**
* Returns the current dictionary, for example "de_CH"
*/
QString currentDictionary() const;
/**
* Sets the current dictionaryName to the given dictionaryName
*/
void setCurrentByDictionaryName(const QString &dictionaryName);
/**
* Sets the current dictionary to the given dictionary
* Return true if dictionary was found.
* @since 5.40
* TODO merge with previous method in kf6
*/
bool assignByDictionnary(const QString &dictionary);
/**
* Sets the current dictionaryName to the given dictionaryName
* Return true if dictionary was found.
* @since 5.40
* TODO merge with previous method in kf6
*/
bool assignDictionnaryName(const QString &name);
/**
* Sets the current dictionary to the given dictionary.
*/
void setCurrentByDictionary(const QString &dictionary);
Q_SIGNALS:
/**
* @em Emitted whenever the current dictionary changes. Either
* by user intervention or on setCurrentByDictionaryName() or on
* setCurrentByDictionary(). For example "de_CH".
*/
void dictionaryChanged(const QString &dictionary);
/**
* @em Emitted whenever the current dictionary changes. Either
* by user intervention or on setCurrentByDictionaryName() or on
* setCurrentByDictionary(). For example "German (Switzerland)".
*/
void dictionaryNameChanged(const QString &dictionaryName);
private:
std::unique_ptr<DictionaryComboBoxPrivate> const d;
Q_PRIVATE_SLOT(d, void slotDictionaryChanged(int))
};
}
#endif
@@ -0,0 +1,526 @@
/*
* highlighter.cpp
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2006 Laurent Montel <montel@kde.org>
* SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "highlighter.h"
#include "languagefilter_p.h"
#include "loader_p.h"
#include "settingsimpl_p.h"
#include "speller.h"
#include "tokenizer_p.h"
#include "ui_debug.h"
#include <QColor>
#include <QEvent>
#include <QHash>
#include <QKeyEvent>
#include <QMetaMethod>
#include <QPlainTextEdit>
#include <QTextCharFormat>
#include <QTextCursor>
#include <QTextEdit>
#include <QTimer>
namespace Sonnet
{
// Cache of previously-determined languages (when using AutoDetectLanguage)
// There is one such cache per block (paragraph)
class LanguageCache : public QTextBlockUserData
{
public:
// Key: QPair<start, length>
// Value: language name
QMap<QPair<int, int>, QString> languages;
// Remove all cached language information after @p pos
void invalidate(int pos)
{
QMutableMapIterator<QPair<int, int>, QString> it(languages);
it.toBack();
while (it.hasPrevious()) {
it.previous();
if (it.key().first + it.key().second >= pos) {
it.remove();
} else {
break;
}
}
}
QString languageAtPos(int pos) const
{
// The data structure isn't really great for such lookups...
QMapIterator<QPair<int, int>, QString> it(languages);
while (it.hasNext()) {
it.next();
if (it.key().first <= pos && it.key().first + it.key().second >= pos) {
return it.value();
}
}
return QString();
}
};
class HighlighterPrivate
{
public:
HighlighterPrivate(Highlighter *qq, const QColor &col)
: textEdit(nullptr)
, plainTextEdit(nullptr)
, spellColor(col)
, q(qq)
{
tokenizer = new WordTokenizer();
active = true;
automatic = false;
autoDetectLanguageDisabled = false;
wordCount = 0;
errorCount = 0;
intraWordEditing = false;
completeRehighlightRequired = false;
spellColor = spellColor.isValid() ? spellColor : Qt::red;
languageFilter = new LanguageFilter(new SentenceTokenizer());
loader = Loader::openLoader();
loader->settings()->restore();
spellchecker = new Sonnet::Speller();
spellCheckerFound = spellchecker->isValid();
rehighlightRequest = new QTimer(q);
q->connect(rehighlightRequest, &QTimer::timeout, q, &Highlighter::slotRehighlight);
if (!spellCheckerFound) {
return;
}
disablePercentage = loader->settings()->disablePercentageWordError();
disableWordCount = loader->settings()->disableWordErrorCount();
completeRehighlightRequired = true;
rehighlightRequest->setInterval(0);
rehighlightRequest->setSingleShot(true);
rehighlightRequest->start();
}
~HighlighterPrivate();
WordTokenizer *tokenizer = nullptr;
LanguageFilter *languageFilter = nullptr;
Loader *loader = nullptr;
Speller *spellchecker = nullptr;
QTextEdit *textEdit = nullptr;
QPlainTextEdit *plainTextEdit = nullptr;
bool active;
bool automatic;
bool autoDetectLanguageDisabled;
bool completeRehighlightRequired;
bool intraWordEditing;
bool spellCheckerFound; // cached d->dict->isValid() value
QMetaObject::Connection contentsChangeConnection;
int disablePercentage = 0;
int disableWordCount = 0;
int wordCount, errorCount;
QTimer *rehighlightRequest = nullptr;
QColor spellColor;
Highlighter *const q;
};
HighlighterPrivate::~HighlighterPrivate()
{
delete spellchecker;
delete languageFilter;
delete tokenizer;
}
Highlighter::Highlighter(QTextEdit *edit, const QColor &_col)
: QSyntaxHighlighter(edit)
, d(new HighlighterPrivate(this, _col))
{
d->textEdit = edit;
d->textEdit->installEventFilter(this);
d->textEdit->viewport()->installEventFilter(this);
}
Highlighter::Highlighter(QPlainTextEdit *edit, const QColor &col)
: QSyntaxHighlighter(edit)
, d(new HighlighterPrivate(this, col))
{
d->plainTextEdit = edit;
setDocument(d->plainTextEdit->document());
d->plainTextEdit->installEventFilter(this);
d->plainTextEdit->viewport()->installEventFilter(this);
}
Highlighter::~Highlighter()
{
if (d->contentsChangeConnection) {
// prevent crash from QSyntaxHighlighter::~QSyntaxHighlighter -> (...) -> QTextDocument::contentsChange() signal emission:
// ASSERT failure in Sonnet::Highlighter: "Called object is not of the correct type (class destructor may have already run)"
QObject::disconnect(d->contentsChangeConnection);
}
}
bool Highlighter::spellCheckerFound() const
{
return d->spellCheckerFound;
}
void Highlighter::slotRehighlight()
{
if (d->completeRehighlightRequired) {
d->wordCount = 0;
d->errorCount = 0;
rehighlight();
} else {
// rehighlight the current para only (undo/redo safe)
QTextCursor cursor;
if (d->textEdit) {
cursor = d->textEdit->textCursor();
} else {
cursor = d->plainTextEdit->textCursor();
}
if (cursor.hasSelection()) {
cursor.clearSelection();
}
cursor.insertText(QString());
}
// if (d->checksDone == d->checksRequested)
// d->completeRehighlightRequired = false;
QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
}
bool Highlighter::automatic() const
{
return d->automatic;
}
bool Highlighter::autoDetectLanguageDisabled() const
{
return d->autoDetectLanguageDisabled;
}
bool Highlighter::intraWordEditing() const
{
return d->intraWordEditing;
}
void Highlighter::setIntraWordEditing(bool editing)
{
d->intraWordEditing = editing;
}
void Highlighter::setAutomatic(bool automatic)
{
if (automatic == d->automatic) {
return;
}
d->automatic = automatic;
if (d->automatic) {
slotAutoDetection();
}
}
void Highlighter::setAutoDetectLanguageDisabled(bool autoDetectDisabled)
{
d->autoDetectLanguageDisabled = autoDetectDisabled;
}
void Highlighter::slotAutoDetection()
{
bool savedActive = d->active;
// don't disable just because 1 of 4 is misspelled.
if (d->automatic && d->wordCount >= 10) {
// tme = Too many errors
/* clang-format off */
bool tme = (d->errorCount >= d->disableWordCount)
&& (d->errorCount * 100 >= d->disablePercentage * d->wordCount);
/* clang-format on */
if (d->active && tme) {
d->active = false;
} else if (!d->active && !tme) {
d->active = true;
}
}
if (d->active != savedActive) {
if (d->active) {
Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
} else {
qCDebug(SONNET_LOG_UI) << "Sonnet: Disabling spell checking, too many errors";
Q_EMIT activeChanged(
tr("Too many misspelled words. "
"As-you-type spell checking disabled."));
}
d->completeRehighlightRequired = true;
d->rehighlightRequest->setInterval(100);
d->rehighlightRequest->setSingleShot(true);
}
}
void Highlighter::setActive(bool active)
{
if (active == d->active) {
return;
}
d->active = active;
rehighlight();
if (d->active) {
Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
} else {
Q_EMIT activeChanged(tr("As-you-type spell checking disabled."));
}
}
bool Highlighter::isActive() const
{
return d->active;
}
void Highlighter::contentsChange(int pos, int add, int rem)
{
// Invalidate the cache where the text has changed
const QTextBlock &lastBlock = document()->findBlock(pos + add - rem);
QTextBlock block = document()->findBlock(pos);
do {
LanguageCache *cache = dynamic_cast<LanguageCache *>(block.userData());
if (cache) {
cache->invalidate(pos - block.position());
}
block = block.next();
} while (block.isValid() && block < lastBlock);
}
static bool hasNotEmptyText(const QString &text)
{
for (int i = 0; i < text.length(); ++i) {
if (!text.at(i).isSpace()) {
return true;
}
}
return false;
}
void Highlighter::highlightBlock(const QString &text)
{
if (!hasNotEmptyText(text) || !d->active || !d->spellCheckerFound) {
return;
}
if (!d->contentsChangeConnection) {
d->contentsChangeConnection = connect(document(), &QTextDocument::contentsChange, this, &Highlighter::contentsChange);
}
d->languageFilter->setBuffer(text);
LanguageCache *cache = dynamic_cast<LanguageCache *>(currentBlockUserData());
if (!cache) {
cache = new LanguageCache;
setCurrentBlockUserData(cache);
}
const bool autodetectLanguage = d->spellchecker->testAttribute(Speller::AutoDetectLanguage);
while (d->languageFilter->hasNext()) {
Token sentence = d->languageFilter->next();
if (autodetectLanguage && !d->autoDetectLanguageDisabled) {
QString lang;
QPair<int, int> spos = QPair<int, int>(sentence.position(), sentence.length());
// try cache first
if (cache->languages.contains(spos)) {
lang = cache->languages.value(spos);
} else {
lang = d->languageFilter->language();
if (!d->languageFilter->isSpellcheckable()) {
lang.clear();
}
cache->languages[spos] = lang;
}
if (lang.isEmpty()) {
continue;
}
d->spellchecker->setLanguage(lang);
}
d->tokenizer->setBuffer(sentence.toString());
int offset = sentence.position();
while (d->tokenizer->hasNext()) {
Token word = d->tokenizer->next();
if (!d->tokenizer->isSpellcheckable()) {
continue;
}
++d->wordCount;
if (d->spellchecker->isMisspelled(word.toString())) {
++d->errorCount;
setMisspelled(word.position() + offset, word.length());
} else {
unsetMisspelled(word.position() + offset, word.length());
}
}
}
// QTimer::singleShot( 0, this, SLOT(checkWords()) );
setCurrentBlockState(0);
}
QString Highlighter::currentLanguage() const
{
return d->spellchecker->language();
}
void Highlighter::setCurrentLanguage(const QString &lang)
{
QString prevLang = d->spellchecker->language();
d->spellchecker->setLanguage(lang);
d->spellCheckerFound = d->spellchecker->isValid();
if (!d->spellCheckerFound) {
qCDebug(SONNET_LOG_UI) << "No dictionary for \"" << lang << "\" staying with the current language.";
d->spellchecker->setLanguage(prevLang);
return;
}
d->wordCount = 0;
d->errorCount = 0;
if (d->automatic || d->active) {
d->rehighlightRequest->start(0);
}
}
void Highlighter::setMisspelled(int start, int count)
{
QTextCharFormat format;
format.setFontUnderline(true);
format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
format.setUnderlineColor(d->spellColor);
setFormat(start, count, format);
}
void Highlighter::unsetMisspelled(int start, int count)
{
setFormat(start, count, QTextCharFormat());
}
bool Highlighter::eventFilter(QObject *o, QEvent *e)
{
if (!d->spellCheckerFound) {
return false;
}
if ((o == d->textEdit || o == d->plainTextEdit) && (e->type() == QEvent::KeyPress)) {
QKeyEvent *k = static_cast<QKeyEvent *>(e);
// d->autoReady = true;
if (d->rehighlightRequest->isActive()) { // try to stay out of the users way
d->rehighlightRequest->start(500);
}
/* clang-format off */
if (k->key() == Qt::Key_Enter
|| k->key() == Qt::Key_Return
|| k->key() == Qt::Key_Up
|| k->key() == Qt::Key_Down
|| k->key() == Qt::Key_Left
|| k->key() == Qt::Key_Right
|| k->key() == Qt::Key_PageUp
|| k->key() == Qt::Key_PageDown
|| k->key() == Qt::Key_Home
|| k->key() == Qt::Key_End
|| (k->modifiers() == Qt::ControlModifier
&& (k->key() == Qt::Key_A
|| k->key() == Qt::Key_B
|| k->key() == Qt::Key_E
|| k->key() == Qt::Key_N
|| k->key() == Qt::Key_P))) { /* clang-format on */
if (intraWordEditing()) {
setIntraWordEditing(false);
d->completeRehighlightRequired = true;
d->rehighlightRequest->setInterval(500);
d->rehighlightRequest->setSingleShot(true);
d->rehighlightRequest->start();
}
} else {
setIntraWordEditing(true);
}
if (k->key() == Qt::Key_Space //
|| k->key() == Qt::Key_Enter //
|| k->key() == Qt::Key_Return) {
QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
}
} else if (((d->textEdit && (o == d->textEdit->viewport())) //
|| (d->plainTextEdit && (o == d->plainTextEdit->viewport()))) //
&& (e->type() == QEvent::MouseButtonPress)) {
// d->autoReady = true;
if (intraWordEditing()) {
setIntraWordEditing(false);
d->completeRehighlightRequired = true;
d->rehighlightRequest->setInterval(0);
d->rehighlightRequest->setSingleShot(true);
d->rehighlightRequest->start();
}
}
return false;
}
void Highlighter::addWordToDictionary(const QString &word)
{
d->spellchecker->addToPersonal(word);
}
void Highlighter::ignoreWord(const QString &word)
{
d->spellchecker->addToSession(word);
}
QStringList Highlighter::suggestionsForWord(const QString &word, int max)
{
QStringList suggestions = d->spellchecker->suggest(word);
if (max >= 0 && suggestions.count() > max) {
suggestions = suggestions.mid(0, max);
}
return suggestions;
}
QStringList Highlighter::suggestionsForWord(const QString &word, const QTextCursor &cursor, int max)
{
LanguageCache *cache = dynamic_cast<LanguageCache *>(cursor.block().userData());
if (cache) {
const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock());
if (!cachedLanguage.isEmpty()) {
d->spellchecker->setLanguage(cachedLanguage);
}
}
QStringList suggestions = d->spellchecker->suggest(word);
if (max >= 0 && suggestions.count() > max) {
suggestions = suggestions.mid(0, max);
}
return suggestions;
}
bool Highlighter::isWordMisspelled(const QString &word)
{
return d->spellchecker->isMisspelled(word);
}
void Highlighter::setMisspelledColor(const QColor &color)
{
d->spellColor = color;
}
bool Highlighter::checkerEnabledByDefault() const
{
return d->loader->settings()->checkerEnabledByDefault();
}
void Highlighter::setDocument(QTextDocument *document)
{
d->contentsChangeConnection = {};
QSyntaxHighlighter::setDocument(document);
}
}
#include "moc_highlighter.cpp"
@@ -0,0 +1,236 @@
/*
* highlighter.h
*
* SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
* SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SONNET_HIGHLIGHTER_H
#define SONNET_HIGHLIGHTER_H
#include "sonnetui_export.h"
#include <QStringList>
#include <QSyntaxHighlighter>
#include <memory>
class QTextEdit;
class QPlainTextEdit;
namespace Sonnet
{
class HighlighterPrivate;
/// The Sonnet Highlighter class, used for drawing pretty red lines in text fields
class SONNETUI_EXPORT Highlighter : public QSyntaxHighlighter
{
Q_OBJECT
public:
explicit Highlighter(QTextEdit *textEdit, const QColor &col = QColor());
/**
* @brief Highlighter
* @param textEdit
* @param col define spellchecking color.
* @since 5.12
*/
explicit Highlighter(QPlainTextEdit *textEdit, const QColor &col = QColor());
~Highlighter() override;
/**
* Returns whether a spell checking backend with support for the
* @ref currentLanguage was found.
*
* @return true if spell checking is supported for the current language.
*/
bool spellCheckerFound() const;
/**
* Returns the current language used for spell checking.
*
* @return the language code for the current language.
*/
QString currentLanguage() const;
/**
* @short Enable/Disable spell checking.
*
* If @p active is true then spell checking is enabled; otherwise it
* is disabled. Note that you have to disable automatic (de)activation
* with @ref setAutomatic() before you change the state of spell
* checking if you want to persistently enable/disable spell
* checking.
*
* @param active if true, then spell checking is enabled
*
* @see isActive(), setAutomatic()
*/
void setActive(bool active);
/**
* Returns the state of spell checking.
*
* @return true if spell checking is active
*
* @see setActive()
*/
bool isActive() const;
/**
* Returns the state of the automatic disabling of spell checking.
*
* @return true if spell checking is automatically disabled if there's
* too many errors
*/
bool automatic() const;
/**
* Sets whether to automatically disable spell checking if there's too
* many errors.
*
* @param automatic if true, spell checking will be disabled if there's
* a significant amount of errors.
*/
void setAutomatic(bool automatic);
/**
* Returns whether the automatic language detection is disabled,
* overriding the Sonnet settings.
*
* @return true if the automatic language detection is disabled
* @since 5.71
*/
bool autoDetectLanguageDisabled() const;
/**
* Sets whether to disable the automatic language detection.
*
* @param autoDetectDisabled if true, the language will not be
* detected automatically by the spell checker, even if the option
* is enabled in the Sonnet settings.
* @since 5.71
*/
void setAutoDetectLanguageDisabled(bool autoDetectDisabled);
/**
* Adds the given word permanently to the dictionary. It will never
* be marked as misspelled again, even after restarting the application.
*
* @param word the word which will be added to the dictionary
* @since 4.1
*/
void addWordToDictionary(const QString &word);
/**
* Ignores the given word. This word will not be marked misspelled for
* this session. It will again be marked as misspelled when creating
* new highlighters.
*
* @param word the word which will be ignored
* @since 4.1
*/
void ignoreWord(const QString &word);
/**
* Returns a list of suggested replacements for the given misspelled word.
* If the word is not misspelled, the list will be empty.
*
* @param word the misspelled word
* @param max at most this many suggestions will be returned. If this is
* -1, as many suggestions as the spell backend supports will
* be returned.
* @return a list of suggested replacements for the word
* @since 4.1
*/
QStringList suggestionsForWord(const QString &word, int max = 10);
/**
* Returns a list of suggested replacements for the given misspelled word.
* If the word is not misspelled, the list will be empty.
*
* @param word the misspelled word
* @param cursor the cursor pointing to the beginning of that word. This is used
* to determine the language to use, when AutoDetectLanguage is enabled.
* @param max at most this many suggestions will be returned. If this is
* -1, as many suggestions as the spell backend supports will
* be returned.
* @return a list of suggested replacements for the word
* @since 5.42
*/
QStringList suggestionsForWord(const QString &word, const QTextCursor &cursor, int max = 10);
/**
* Checks if a given word is marked as misspelled by the highlighter.
*
* @param word the word to be checked
* @return true if the given word is misspelled.
* @since 4.1
*/
bool isWordMisspelled(const QString &word);
/**
* Sets the color in which the highlighter underlines misspelled words.
* @since 4.2
*/
void setMisspelledColor(const QColor &color);
/**
* Return true if checker is enabled by default
* @since 4.5
*/
bool checkerEnabledByDefault() const;
/**
* Set a new @ref QTextDocument for this highlighter to operate on.
*
* @param document the new document to operate on.
*/
void setDocument(QTextDocument *document);
Q_SIGNALS:
/**
* Emitted when as-you-type spell checking is enabled or disabled.
*
* @param description is a i18n description of the new state,
* with an optional reason
*/
void activeChanged(const QString &description);
protected:
void highlightBlock(const QString &text) override;
virtual void setMisspelled(int start, int count);
virtual void unsetMisspelled(int start, int count);
bool eventFilter(QObject *o, QEvent *e) override;
bool intraWordEditing() const;
void setIntraWordEditing(bool editing);
public Q_SLOTS:
/**
* Set language to use for spell checking.
*
* @param language the language code for the new language to use.
*/
void setCurrentLanguage(const QString &language);
/**
* Run auto detection, disabling spell checking if too many errors are found.
*/
void slotAutoDetection();
/**
* Force a new highlighting.
*/
void slotRehighlight();
private Q_SLOTS:
SONNETUI_NO_EXPORT void contentsChange(int pos, int added, int removed);
private:
std::unique_ptr<HighlighterPrivate> const d;
Q_DISABLE_COPY(Highlighter)
};
}
#endif
@@ -0,0 +1,17 @@
TARGET = sonnet-ui
TEMPLATE = lib
CONFIG += staticlib
QT += widgets
SOURCES += highlighter.cpp
HEADERS += highlighter.h
DEFINES += SONNETUI_EXPORT=""
DEFINES += SONNETCORE_EXPORT=""
DEFINES += INSTALLATION_PLUGIN_PATH=""
DEFINES += SONNET_STATIC
INCLUDEPATH += ../core
unix:system("touch sonnetui_export.h")
win32:system("type nul > sonnetui_export.h")
@@ -0,0 +1,313 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SonnetUi</class>
<widget class="QWidget" name="SonnetUi">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>481</width>
<height>311</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>430</width>
<height>300</height>
</size>
</property>
<layout class="QGridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="textLabel2">
<property name="whatsThis">
<string>&lt;qt&gt;&lt;p&gt;This word was considered to be an &quot;unknown word&quot; because it does not match any entry in the dictionary currently in use. It may also be a word in a foreign language.&lt;/p&gt;
&lt;p&gt;If the word is not misspelled, you may add it to the dictionary by clicking &lt;b&gt;Add to Dictionary&lt;/b&gt;. If you do not want to add the unknown word to the dictionary, but you want to leave it unchanged, click &lt;b&gt;Ignore&lt;/b&gt; or &lt;b&gt;Ignore All&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;However, if the word is misspelled, you can try to find the correct replacement in the list below. If you cannot find a replacement there, you may type it in the text box below, and click &lt;b&gt;Replace&lt;/b&gt; or &lt;b&gt;Replace All&lt;/b&gt;.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>Unknown word:</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="m_unknownWord">
<property name="toolTip">
<string>Unknown word</string>
</property>
<property name="whatsThis">
<string>&lt;qt&gt;&lt;p&gt;This word was considered to be an &quot;unknown word&quot; because it does not match any entry in the dictionary currently in use. It may also be a word in a foreign language.&lt;/p&gt;
&lt;p&gt;If the word is not misspelled, you may add it to the dictionary by clicking &lt;b&gt;Add to Dictionary&lt;/b&gt;. If you do not want to add the unknown word to the dictionary, but you want to leave it unchanged, click &lt;b&gt;Ignore&lt;/b&gt; or &lt;b&gt;Ignore All&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;However, if the word is misspelled, you can try to find the correct replacement in the list below. If you cannot find a replacement there, you may type it in the text box below, and click &lt;b&gt;Replace&lt;/b&gt; or &lt;b&gt;Replace All&lt;/b&gt;.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>&lt;b&gt;misspelled&lt;/b&gt;</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="textLabel5">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Select the language of the document you are proofing here.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>&amp;Language:</string>
</property>
<property name="buddy">
<cstring>m_language</cstring>
</property>
</widget>
</item>
<item row="0" column="0" colspan="6">
<widget class="QLabel" name="m_contextLabel">
<property name="toolTip">
<string>Text excerpt showing the unknown word in its context.</string>
</property>
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Here you can see a text excerpt showing the unknown word in its context. If this information is not sufficient to choose the best replacement for the unknown word, you can click on the document you are proofing, read a larger part of the text and then return here to continue proofing.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string>... the &lt;b&gt;misspelled&lt;/b&gt; word shown in context ...</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="4" colspan="2">
<widget class="QPushButton" name="m_addBtn">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;The unknown word was detected and considered unknown because it is not included in the dictionary.&lt;br&gt;
Click here if you consider the unknown word not to be misspelled, and you want to avoid wrongly detecting it again in the future. If you want to let it remain as is, but not add it to the dictionary, then click &lt;b&gt;Ignore&lt;/b&gt; or &lt;b&gt;Ignore All&lt;/b&gt; instead.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>&lt;&lt; Add to Dictionary</string>
</property>
</widget>
</item>
<item row="1" column="3">
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>74</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0" colspan="5">
<widget class="QListView" name="m_suggestions">
<property name="toolTip">
<string>Suggestion List</string>
</property>
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;If the unknown word is misspelled, you should check if the correction for it is available and if it is, click on it. If none of the words in this list is a good replacement you may type the correct word in the edit box above.&lt;/p&gt;
&lt;p&gt;To correct this word click &lt;b&gt;Replace&lt;/b&gt; if you want to correct only this occurrence or &lt;b&gt;Replace All&lt;/b&gt; if you want to correct all occurrences.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="textLabel4">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;If the unknown word is misspelled, you should type the correction for your misspelled word here or select it from the list below.&lt;/p&gt;
&lt;p&gt;You can then click &lt;b&gt;Replace&lt;/b&gt; if you want to correct only this occurrence of the word or &lt;b&gt;Replace All&lt;/b&gt; if you want to correct all occurrences.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>Replace &amp;with:</string>
</property>
<property name="buddy">
<cstring>m_replacement</cstring>
</property>
</widget>
</item>
<item row="2" column="2" colspan="3">
<widget class="QLineEdit" name="m_replacement">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;If the unknown word is misspelled, you should type the correction for your misspelled word here or select it from the list below.&lt;/p&gt;
&lt;p&gt;You can then click &lt;b&gt;Replace&lt;/b&gt; if you want to correct only this occurrence of the word or &lt;b&gt;Replace All&lt;/b&gt; if you want to correct all occurrences.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="4">
<widget class="Sonnet::DictionaryComboBox" name="m_language">
<property name="toolTip">
<string>Language Selection</string>
</property>
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Select the language of the document you are proofing here.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
</widget>
</item>
<item row="2" column="5" rowspan="3">
<layout class="QVBoxLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="m_suggestBtn">
<property name="text">
<string>S&amp;uggest</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_replaceBtn">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Click here to replace this occurrence of the unknown text with the text in the edit box above (to the left).&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>&amp;Replace</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_replaceAllBtn">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Click here to replace all occurrences of the unknown text with the text in the edit box above (to the left).&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>R&amp;eplace All</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_skipBtn">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Click here to let this occurrence of the unknown word remain as is.&lt;/p&gt;
&lt;p&gt;This action is useful when the word is a name, an acronym, a foreign word or any other unknown word that you want to use but not add to the dictionary.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>&amp;Ignore</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_skipAllBtn">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Click here to let all occurrences of the unknown word remain as they are.&lt;/p&gt;
&lt;p&gt;This action is useful when the word is a name, an acronym, a foreign word or any other unknown word that you want to use but not add to the dictionary.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>I&amp;gnore All</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_autoCorrect">
<property name="whatsThis">
<string>&lt;qt&gt;
&lt;p&gt;Click here to let all occurrences of the unknown word remain as they are.&lt;/p&gt;
&lt;p&gt;This action is useful when the word is a name, an acronym, a foreign word or any other unknown word that you want to use but not add to the dictionary.&lt;/p&gt;
&lt;/qt&gt;</string>
</property>
<property name="text">
<string>Autocorrect</string>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Sonnet::DictionaryComboBox</class>
<extends>QComboBox</extends>
<header>sonnet/dictionarycombobox.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>m_addBtn</tabstop>
<tabstop>m_replacement</tabstop>
<tabstop>m_suggestBtn</tabstop>
<tabstop>m_replaceBtn</tabstop>
<tabstop>m_replaceAllBtn</tabstop>
<tabstop>m_skipBtn</tabstop>
<tabstop>m_skipAllBtn</tabstop>
<tabstop>m_suggestions</tabstop>
<tabstop>m_language</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,268 @@
/*
* spellcheckdecorator.h
*
* SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "spellcheckdecorator.h"
// Local
#include <highlighter.h>
// Qt
#include <QContextMenuEvent>
#include <QMenu>
#include <QPlainTextEdit>
#include <QTextEdit>
namespace Sonnet
{
class SpellCheckDecoratorPrivate
{
public:
SpellCheckDecoratorPrivate(SpellCheckDecorator *installer, QPlainTextEdit *textEdit)
: q(installer)
, m_plainTextEdit(textEdit)
{
createDefaultHighlighter();
// Catch pressing the "menu" key
m_plainTextEdit->installEventFilter(q);
// Catch right-click
m_plainTextEdit->viewport()->installEventFilter(q);
}
SpellCheckDecoratorPrivate(SpellCheckDecorator *installer, QTextEdit *textEdit)
: q(installer)
, m_textEdit(textEdit)
{
createDefaultHighlighter();
// Catch pressing the "menu" key
m_textEdit->installEventFilter(q);
// Catch right-click
m_textEdit->viewport()->installEventFilter(q);
}
~SpellCheckDecoratorPrivate()
{
if (m_plainTextEdit) {
m_plainTextEdit->removeEventFilter(q);
m_plainTextEdit->viewport()->removeEventFilter(q);
}
if (m_textEdit) {
m_textEdit->removeEventFilter(q);
m_textEdit->viewport()->removeEventFilter(q);
}
}
bool onContextMenuEvent(QContextMenuEvent *event);
void execSuggestionMenu(const QPoint &pos, const QString &word, const QTextCursor &cursor);
void createDefaultHighlighter();
SpellCheckDecorator *const q;
QTextEdit *m_textEdit = nullptr;
QPlainTextEdit *m_plainTextEdit = nullptr;
Highlighter *m_highlighter = nullptr;
};
bool SpellCheckDecoratorPrivate::onContextMenuEvent(QContextMenuEvent *event)
{
if (!m_highlighter) {
createDefaultHighlighter();
}
// Obtain the cursor at the mouse position and the current cursor
QTextCursor cursorAtMouse;
if (m_textEdit) {
cursorAtMouse = m_textEdit->cursorForPosition(event->pos());
} else {
cursorAtMouse = m_plainTextEdit->cursorForPosition(event->pos());
}
const int mousePos = cursorAtMouse.position();
QTextCursor cursor;
if (m_textEdit) {
cursor = m_textEdit->textCursor();
} else {
cursor = m_plainTextEdit->textCursor();
}
// Check if the user clicked a selected word
/* clang-format off */
const bool selectedWordClicked = cursor.hasSelection()
&& mousePos >= cursor.selectionStart()
&& mousePos <= cursor.selectionEnd();
/* clang-format on */
// Get the word under the (mouse-)cursor and see if it is misspelled.
// Don't include apostrophes at the start/end of the word in the selection.
QTextCursor wordSelectCursor(cursorAtMouse);
wordSelectCursor.clearSelection();
wordSelectCursor.select(QTextCursor::WordUnderCursor);
QString selectedWord = wordSelectCursor.selectedText();
bool isMouseCursorInsideWord = true;
if ((mousePos < wordSelectCursor.selectionStart() || mousePos >= wordSelectCursor.selectionEnd()) //
&& (selectedWord.length() > 1)) {
isMouseCursorInsideWord = false;
}
// Clear the selection again, we re-select it below (without the apostrophes).
wordSelectCursor.setPosition(wordSelectCursor.position() - selectedWord.size());
if (selectedWord.startsWith(QLatin1Char('\'')) || selectedWord.startsWith(QLatin1Char('\"'))) {
selectedWord = selectedWord.right(selectedWord.size() - 1);
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
}
if (selectedWord.endsWith(QLatin1Char('\'')) || selectedWord.endsWith(QLatin1Char('\"'))) {
selectedWord.chop(1);
}
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selectedWord.size());
/* clang-format off */
const bool wordIsMisspelled = isMouseCursorInsideWord
&& m_highlighter
&& m_highlighter->isActive()
&& !selectedWord.isEmpty()
&& m_highlighter->isWordMisspelled(selectedWord);
/* clang-format on */
// If the user clicked a selected word, do nothing.
// If the user clicked somewhere else, move the cursor there.
// If the user clicked on a misspelled word, select that word.
// Same behavior as in OpenOffice Writer.
bool checkBlock = q->isSpellCheckingEnabledForBlock(cursorAtMouse.block().text());
if (!selectedWordClicked) {
if (wordIsMisspelled && checkBlock) {
if (m_textEdit) {
m_textEdit->setTextCursor(wordSelectCursor);
} else {
m_plainTextEdit->setTextCursor(wordSelectCursor);
}
} else {
if (m_textEdit) {
m_textEdit->setTextCursor(cursorAtMouse);
} else {
m_plainTextEdit->setTextCursor(cursorAtMouse);
}
}
if (m_textEdit) {
cursor = m_textEdit->textCursor();
} else {
cursor = m_plainTextEdit->textCursor();
}
}
// Use standard context menu for already selected words, correctly spelled
// words and words inside quotes.
if (!wordIsMisspelled || selectedWordClicked || !checkBlock) {
return false;
}
execSuggestionMenu(event->globalPos(), selectedWord, cursor);
return true;
}
void SpellCheckDecoratorPrivate::execSuggestionMenu(const QPoint &pos, const QString &selectedWord, const QTextCursor &_cursor)
{
QTextCursor cursor = _cursor;
QMenu menu; // don't use KMenu here we don't want auto management accelerator
// Add the suggestions to the menu
const QStringList reps = m_highlighter->suggestionsForWord(selectedWord, cursor);
if (reps.isEmpty()) {
QAction *suggestionsAction = menu.addAction(SpellCheckDecorator::tr("No suggestions for %1").arg(selectedWord));
suggestionsAction->setEnabled(false);
} else {
QStringList::const_iterator end(reps.constEnd());
for (QStringList::const_iterator it = reps.constBegin(); it != end; ++it) {
menu.addAction(*it);
}
}
menu.addSeparator();
QAction *ignoreAction = menu.addAction(SpellCheckDecorator::tr("Ignore"));
QAction *addToDictAction = menu.addAction(SpellCheckDecorator::tr("Add to Dictionary"));
// Execute the popup inline
const QAction *selectedAction = menu.exec(pos);
if (selectedAction) {
// Fails when we're in the middle of a compose-key sequence
// Q_ASSERT(cursor.selectedText() == selectedWord);
if (selectedAction == ignoreAction) {
m_highlighter->ignoreWord(selectedWord);
m_highlighter->rehighlight();
} else if (selectedAction == addToDictAction) {
m_highlighter->addWordToDictionary(selectedWord);
m_highlighter->rehighlight();
}
// Other actions can only be one of the suggested words
else {
const QString replacement = selectedAction->text();
Q_ASSERT(reps.contains(replacement));
cursor.insertText(replacement);
if (m_textEdit) {
m_textEdit->setTextCursor(cursor);
} else {
m_plainTextEdit->setTextCursor(cursor);
}
}
}
}
void SpellCheckDecoratorPrivate::createDefaultHighlighter()
{
if (m_textEdit) {
m_highlighter = new Highlighter(m_textEdit);
} else {
m_highlighter = new Highlighter(m_plainTextEdit);
}
}
SpellCheckDecorator::SpellCheckDecorator(QTextEdit *textEdit)
: QObject(textEdit)
, d(std::make_unique<SpellCheckDecoratorPrivate>(this, textEdit))
{
}
SpellCheckDecorator::SpellCheckDecorator(QPlainTextEdit *textEdit)
: QObject(textEdit)
, d(std::make_unique<SpellCheckDecoratorPrivate>(this, textEdit))
{
}
SpellCheckDecorator::~SpellCheckDecorator() = default;
void SpellCheckDecorator::setHighlighter(Highlighter *highlighter)
{
d->m_highlighter = highlighter;
}
Highlighter *SpellCheckDecorator::highlighter() const
{
if (!d->m_highlighter) {
d->createDefaultHighlighter();
}
return d->m_highlighter;
}
bool SpellCheckDecorator::eventFilter(QObject * /*obj*/, QEvent *event)
{
if (event->type() == QEvent::ContextMenu) {
return d->onContextMenuEvent(static_cast<QContextMenuEvent *>(event));
}
return false;
}
bool SpellCheckDecorator::isSpellCheckingEnabledForBlock(const QString &textBlock) const
{
Q_UNUSED(textBlock);
if (d->m_textEdit) {
return d->m_textEdit->isEnabled();
} else {
return d->m_plainTextEdit->isEnabled();
}
}
} // namespace
#include "moc_spellcheckdecorator.cpp"
@@ -0,0 +1,85 @@
/*
* spellcheckdecorator.h
*
* SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef SPELLCHECKDECORATOR_H
#define SPELLCHECKDECORATOR_H
#include <QObject>
#include <memory>
#include "sonnetui_export.h"
class QTextEdit;
class QPlainTextEdit;
namespace Sonnet
{
class SpellCheckDecoratorPrivate;
class Highlighter;
/**
* @class Sonnet::SpellCheckDecorator spellcheckdecorator.h <Sonnet/SpellCheckDecorator>
*
* @short Connects a Sonnet::Highlighter to a QTextEdit extending the context menu
* of the text edit with spell check suggestions
* @author Aurélien Gâteau <agateau@kde.org>
* @since 5.0
**/
class SONNETUI_EXPORT SpellCheckDecorator : public QObject
{
Q_OBJECT
public:
/**
* Creates a spell-check decorator.
*
* @param textEdit the QTextEdit in need of spell-checking. It also is used as the QObject parent for the decorator.
*/
explicit SpellCheckDecorator(QTextEdit *textEdit);
/**
* Creates a spell-check decorator.
*
* @param textEdit the QPlainTextEdit in need of spell-checking. It also is used as the QObject parent for the decorator.
* @since 5.12
*/
explicit SpellCheckDecorator(QPlainTextEdit *textEdit);
~SpellCheckDecorator() override;
/**
* Set a custom highlighter on the decorator.
*
* SpellCheckDecorator does not take ownership of the new highlighter,
* and you must manually delete the old highlighter.
*/
void setHighlighter(Highlighter *highlighter);
/**
* Returns the hightlighter used by the decorator
*/
Highlighter *highlighter() const;
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
/**
* Returns true if the spell checking should be enabled for a given block of text
* The default implementation always returns true.
*/
virtual bool isSpellCheckingEnabledForBlock(const QString &textBlock) const;
private:
friend SpellCheckDecoratorPrivate;
const std::unique_ptr<SpellCheckDecoratorPrivate> d;
Q_DISABLE_COPY(SpellCheckDecorator)
};
}
#endif /* SPELLCHECKDECORATOR_H */