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,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