kcms/spellchecking: Port to QML

BUG: 432956
This commit is contained in:
Tobias Fella 2026-08-28 12:25:21 +02:00
parent 78cc8a6e83
commit 00613d23e4
8 changed files with 316 additions and 266 deletions

View file

@ -1,16 +1,13 @@
set(kcm_spellchecking_SRCS
spellcheckingskeleton.cpp
spellchecking.cpp
spellcheckingdata.cpp
)
# SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
add_library(kcm_spellchecking MODULE ${kcm_spellchecking_SRCS})
kcmutils_generate_desktop_file(kcm_spellchecking)
kcmutils_add_qml_kcm(kcm_spellchecking SOURCES
spellchecking.cpp
spellcheckingdata.cpp
)
target_link_libraries(kcm_spellchecking
KF6::KCMUtils
KF6::SonnetCore
KF6::SonnetUi
)
install(TARGETS kcm_spellchecking DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/kcms/systemsettings_qwidgets )
KF6::KCMUtils
KF6::SonnetCore
KF6::SonnetUi
)

View file

@ -7,112 +7,37 @@
#include "spellchecking.h"
#include <QBoxLayout>
#include <QSet>
#include "spellcheckingdata.h"
#include <KConfigDialogManager>
#include <KPluginFactory>
#include <Sonnet/ConfigView>
#include <Sonnet/Settings>
K_PLUGIN_FACTORY_WITH_JSON(SpellFactory, "kcm_spellchecking.json", registerPlugin<SonnetSpellCheckingModule>(); registerPlugin<SpellCheckingData>();)
SonnetSpellCheckingModule::SonnetSpellCheckingModule(QObject *parent, const KPluginMetaData &data)
: KCModule(parent, data)
, m_data(new SpellCheckingData(this))
: KQuickConfigModule(parent, data)
, m_settings(new Sonnet::Settings(this))
{
QBoxLayout *layout = new QVBoxLayout(widget());
layout->setContentsMargins(0, 0, 0, 0);
m_configWidget = new Sonnet::ConfigView(widget());
m_configWidget->setNoBackendFoundVisible(skeleton()->clients().isEmpty());
layout->addWidget(m_configWidget);
m_managedConfig = addConfig(skeleton(), m_configWidget);
connect(m_configWidget, &Sonnet::ConfigView::configChanged, this, &SonnetSpellCheckingModule::stateChanged);
}
void SonnetSpellCheckingModule::stateChanged()
{
bool unmanagedChangeState = false;
bool unmanagedDefaultState = true;
QStringList refIgnoreList(skeleton()->ignoreList());
refIgnoreList.removeDuplicates();
refIgnoreList.sort();
QStringList currentIgnoreList(m_configWidget->ignoreList());
currentIgnoreList.removeDuplicates();
currentIgnoreList.sort();
QStringList defaultIgnoreList(Sonnet::Settings::defaultIgnoreList());
defaultIgnoreList.removeDuplicates();
defaultIgnoreList.sort();
unmanagedChangeState |= currentIgnoreList != refIgnoreList;
unmanagedDefaultState &= currentIgnoreList == defaultIgnoreList;
QStringList refPreferredLanguagesList(skeleton()->preferredLanguages());
refPreferredLanguagesList.removeDuplicates();
refPreferredLanguagesList.sort();
QStringList currentPreferredLanguagesList(m_configWidget->preferredLanguages());
currentPreferredLanguagesList.removeDuplicates();
currentPreferredLanguagesList.sort();
QStringList defaultPreferredLanguagesList(Sonnet::Settings::defaultPreferredLanguages());
defaultPreferredLanguagesList.removeDuplicates();
defaultPreferredLanguagesList.sort();
unmanagedChangeState |= currentPreferredLanguagesList != refPreferredLanguagesList;
unmanagedDefaultState &= currentPreferredLanguagesList == defaultPreferredLanguagesList;
if (!skeleton()->clients().isEmpty() && !m_configWidget->language().isEmpty()) {
unmanagedChangeState |= skeleton()->defaultLanguage() != m_configWidget->language();
unmanagedDefaultState &= m_configWidget->language() == Sonnet::Settings::defaultDefaultLanguage();
}
unmanagedWidgetDefaultState(unmanagedDefaultState);
unmanagedWidgetChangeState(unmanagedChangeState);
}
SonnetSpellCheckingModule::~SonnetSpellCheckingModule()
{
}
void SonnetSpellCheckingModule::load()
{
KCModule::load();
// Set unmanaged widget value
m_configWidget->setIgnoreList(skeleton()->ignoreList());
m_configWidget->setPreferredLanguages(skeleton()->preferredLanguages());
m_configWidget->setLanguage(skeleton()->defaultLanguage());
connect(m_settings, &Sonnet::Settings::modifiedChanged, this, [this] {
setNeedsSave(true);
});
}
void SonnetSpellCheckingModule::save()
{
skeleton()->setIgnoreList(m_configWidget->ignoreList());
skeleton()->setPreferredLanguages(m_configWidget->preferredLanguages());
skeleton()->setDefaultLanguage(m_configWidget->language());
// with addConfig, save on skeleton will be trigger only if one managed widget changed
if (!m_managedConfig->hasChanged()) {
skeleton()->save();
}
KCModule::save();
m_settings->save();
}
void SonnetSpellCheckingModule::defaults()
{
KCModule::defaults();
// set default value for unmanaged widgets
m_configWidget->setIgnoreList(Sonnet::Settings::defaultIgnoreList());
m_configWidget->setPreferredLanguages(Sonnet::Settings::defaultPreferredLanguages());
m_configWidget->setLanguage(Sonnet::Settings::defaultDefaultLanguage());
}
SpellCheckingSkeleton *SonnetSpellCheckingModule::skeleton() const
{
return m_data->settings();
m_settings->setCurrentIgnoreList(Sonnet::Settings::defaultIgnoreList());
m_settings->setSkipUppercase(Sonnet::Settings::defaultSkipUppercase());
m_settings->setAutodetectLanguage(Sonnet::Settings::defaultAutodetectLanguage());
m_settings->setBackgroundCheckerEnabled(Sonnet::Settings::defaultBackgroundCheckerEnabled());
m_settings->setCheckerEnabledByDefault(Sonnet::Settings::defaultCheckerEnabledByDefault());
m_settings->setSkipRunTogether(Sonnet::Settings::defaultSkipRunTogether());
m_settings->setDefaultLanguage(Sonnet::Settings::defaultDefaultLanguage());
m_settings->setPreferredLanguages(Sonnet::Settings::defaultPreferredLanguages());
}
#include "spellchecking.moc"

View file

@ -1,44 +1,33 @@
/*
SPDX-FileCopyrightText: 2008 Albert Astals Cid <aacid@kde.org>
SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include "spellcheckingskeleton.h"
#include <KCModule>
#include <KQuickConfigModule>
class KConfigDialogManager;
class SpellCheckingData;
class SpellCheckingSkeleton;
namespace Sonnet
{
class ConfigView;
class Settings;
}
class SonnetSpellCheckingModule : public KCModule
class SonnetSpellCheckingModule : public KQuickConfigModule
{
Q_OBJECT
Q_PROPERTY(Sonnet::Settings *settings MEMBER m_settings CONSTANT)
public:
SonnetSpellCheckingModule(QObject *parent, const KPluginMetaData &data);
~SonnetSpellCheckingModule() override;
void save() override;
void load() override;
void defaults() override;
SpellCheckingSkeleton *skeleton() const;
private:
void stateChanged();
Sonnet::Settings *m_settings;
Sonnet::ConfigView *m_configWidget;
SpellCheckingData *m_data;
KConfigDialogManager *m_managedConfig;
};

View file

@ -10,45 +10,51 @@
#include <Sonnet/Settings>
#include "spellcheckingskeleton.h"
SpellCheckingData::SpellCheckingData(QObject *parent)
: KCModuleData(parent)
, m_settings(new SpellCheckingSkeleton(this))
, m_settings(new Sonnet::Settings(this))
{
autoRegisterSkeletons();
}
SpellCheckingSkeleton *SpellCheckingData::settings() const
{
return m_settings;
}
bool SpellCheckingData::isDefaults() const
{
bool isDefaults = KCModuleData::isDefaults();
if (m_settings->skipUppercase() != Sonnet::Settings::defaultSkipUppercase()) {
return false;
}
QStringList refIgnoreList(m_settings->ignoreList());
refIgnoreList.removeDuplicates();
refIgnoreList.sort();
if (m_settings->autodetectLanguage() != Sonnet::Settings::defaultAutodetectLanguage()) {
return false;
}
QStringList defaultIgnoreList(Sonnet::Settings::defaultIgnoreList());
defaultIgnoreList.removeDuplicates();
defaultIgnoreList.sort();
if (m_settings->backgroundCheckerEnabled() != Sonnet::Settings::defaultBackgroundCheckerEnabled()) {
return false;
}
QStringList refPreferredLanguagesList(m_settings->preferredLanguages());
refPreferredLanguagesList.removeDuplicates();
refPreferredLanguagesList.sort();
if (m_settings->checkerEnabledByDefault() != Sonnet::Settings::defaultCheckerEnabledByDefault()) {
return false;
}
QStringList defaultPreferredLanguagesList(Sonnet::Settings::defaultPreferredLanguages());
defaultPreferredLanguagesList.removeDuplicates();
defaultPreferredLanguagesList.sort();
if (m_settings->skipRunTogether() != Sonnet::Settings::defaultSkipRunTogether()) {
return false;
}
isDefaults &= refIgnoreList == defaultIgnoreList;
isDefaults &= refPreferredLanguagesList == defaultPreferredLanguagesList;
isDefaults &= m_settings->defaultLanguage() == Sonnet::Settings::defaultDefaultLanguage();
if (m_settings->defaultLanguage() != Sonnet::Settings::defaultDefaultLanguage()) {
return false;
}
return isDefaults;
const auto &currentIgnoreList = m_settings->currentIgnoreList();
const auto &defaultIgnoreList = Sonnet::Settings::defaultIgnoreList();
if (QSet(currentIgnoreList.begin(), currentIgnoreList.end()) != QSet(defaultIgnoreList.begin(), defaultIgnoreList.end())) {
return false;
}
const auto &currentPreferredLanguages = m_settings->preferredLanguages();
const auto &defaultPreferredLanguages = Sonnet::Settings::defaultPreferredLanguages();
if (QSet(currentPreferredLanguages.begin(), currentPreferredLanguages.end()) != QSet(defaultPreferredLanguages.begin(), defaultPreferredLanguages.end())) {
return false;
}
return KCModuleData::isDefaults();
}
#include "moc_spellcheckingdata.cpp"

View file

@ -10,7 +10,10 @@
#include "kcmoduledata.h"
class SpellCheckingSkeleton;
namespace Sonnet
{
class Settings;
}
class SpellCheckingData : public KCModuleData
{
@ -18,9 +21,8 @@ class SpellCheckingData : public KCModuleData
public:
explicit SpellCheckingData(QObject *parent = nullptr);
SpellCheckingSkeleton *settings() const;
bool isDefaults() const override;
private:
SpellCheckingSkeleton *m_settings;
Sonnet::Settings *m_settings;
};

View file

@ -1,76 +0,0 @@
/*
SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "spellcheckingskeleton.h"
#include <Sonnet/ConfigView>
#include <Sonnet/Settings>
SpellCheckingSkeleton::SpellCheckingSkeleton(QObject *parent)
: KCoreConfigSkeleton(QString(), parent)
, m_store(new Sonnet::Settings(this))
{
// Associated with managed widget
addItem(new KPropertySkeletonItem(m_store, "skipUppercase", Sonnet::Settings::defaultSkipUppercase()), "skipUppercase");
addItem(new KPropertySkeletonItem(m_store, "autodetectLanguage", Sonnet::Settings::defaultAutodetectLanguage()), "autodetectLanguage");
addItem(new KPropertySkeletonItem(m_store, "backgroundCheckerEnabled", Sonnet::Settings::defaultBackgroundCheckerEnabled()), "backgroundCheckerEnabled");
addItem(new KPropertySkeletonItem(m_store, "checkerEnabledByDefault", Sonnet::Settings::defaultCheckerEnabledByDefault()), "checkerEnabledByDefault");
addItem(new KPropertySkeletonItem(m_store, "skipRunTogether", Sonnet::Settings::defaultSkipRunTogether()), "skipRunTogether");
usrRead();
}
void SpellCheckingSkeleton::usrRead()
{
m_ignoreList = m_store->currentIgnoreList();
m_preferredLanguages = m_store->preferredLanguages();
m_defaultLanguage = m_store->defaultLanguage();
KCoreConfigSkeleton::usrRead();
}
bool SpellCheckingSkeleton::usrSave()
{
m_store->setCurrentIgnoreList(m_ignoreList);
m_store->setPreferredLanguages(m_preferredLanguages);
m_store->setDefaultLanguage(m_defaultLanguage);
m_store->save();
return KCoreConfigSkeleton::usrSave();
}
void SpellCheckingSkeleton::setPreferredLanguages(const QStringList &preferredLanguages)
{
m_preferredLanguages = preferredLanguages;
}
QStringList SpellCheckingSkeleton::preferredLanguages() const
{
return m_preferredLanguages;
}
void SpellCheckingSkeleton::setIgnoreList(const QStringList &ignoreList)
{
m_ignoreList = ignoreList;
}
QStringList SpellCheckingSkeleton::ignoreList() const
{
return m_ignoreList;
}
void SpellCheckingSkeleton::setDefaultLanguage(const QString &defaultLanguage)
{
m_defaultLanguage = defaultLanguage;
}
QString SpellCheckingSkeleton::defaultLanguage() const
{
return m_defaultLanguage;
}
QStringList SpellCheckingSkeleton::clients() const
{
return m_store->clients();
}
#include "moc_spellcheckingskeleton.cpp"

View file

@ -1,42 +0,0 @@
/*
SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include <KCoreConfigSkeleton>
namespace Sonnet
{
class Settings;
class ConfigView;
}
class SpellCheckingSkeleton : public KCoreConfigSkeleton
{
Q_OBJECT
public:
explicit SpellCheckingSkeleton(QObject *parent = nullptr);
bool usrSave() override;
void usrRead() override;
void setPreferredLanguages(const QStringList &preferredLanguages);
QStringList preferredLanguages() const;
void setIgnoreList(const QStringList &ignoreList);
QStringList ignoreList() const;
void setDefaultLanguage(const QString &defaultLanguage);
QString defaultLanguage() const;
QStringList clients() const;
private:
Sonnet::Settings *m_store;
QStringList m_preferredLanguages;
QStringList m_ignoreList;
QString m_defaultLanguage;
};

View file

@ -0,0 +1,249 @@
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import QtQml
import org.kde.kirigami as Kirigami
import org.kde.kirigami.delegates as KD
import org.kde.kirigami.dialogs
import org.kde.kcmutils as KCM
import org.kde.kitemmodels
KCM.SimpleKCM {
id: root
Kirigami.Form {
Kirigami.FormGroup {
Kirigami.FormEntry {
contentItem: QQC2.ComboBox {
id: defaultLanguage
Kirigami.FormData.label: i18nc("@label", "Default Language:")
model: kcm.settings.dictionaryModel
textRole: "display"
valueRole: "languageCode"
onActivated: {
kcm.settings.defaultLanguage = currentValue
}
Component.onCompleted: {
currentIndex = indexOfValue(kcm.settings.defaultLanguage)
}
Connections {
target: kcm.settings
function onDefaultLanguageChanged(): void {
defaultLanguage.currentIndex = defaultLanguage.indexOfValue(kcm.settings.defaultLanguage)
}
}
}
KCM.SettingHighlighter {
highlight: kcm.settings.defaultLanguage !== kcm.settings.defaultDefaultLanguage
}
}
}
Kirigami.FormGroup {
title: i18nc("@title", "Preferred Languages")
Kirigami.FormEntry {
contentItem: QQC2.Button {
text: i18nc("@action:button", "Configure Preferred Languages…")
icon.name: "languages"
onClicked: {
let dialog = spellCheckingLanguageList.createObject(root) as Kirigami.Dialog;
dialog.open();
}
KCM.SettingHighlighter {
highlight: kcm.settings.preferredLanguages.length !== kcm.settings.defaultPreferredLanguages.length || !kcm.settings.preferredLanguages.every(value => kcm.settings.defaultPreferredLanguages.includes(value))
}
Component {
id: spellCheckingLanguageList
Kirigami.Dialog {
id: scroll
title: i18ndc("kirigami-addons6", "@title:window", "Spell Checking Languages")
width: Kirigami.Units.gridUnit * 24
standardButtons: QQC2.DialogButtonBox.Close
onRejected: scroll.close()
header: DialogHeader {
dialog: scroll
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.DialogHeaderTopContent {
Layout.fillWidth: true
dialog: scroll
}
Kirigami.SearchField {
id: searchField
Layout.fillWidth: true
}
}
}
ListView {
id: listView
clip: true
model: KSortFilterProxyModel {
sourceModel: kcm.settings.dictionaryModel
filterRoleName: "display"
filterString: searchField.text
}
delegate: QQC2.CheckDelegate {
id: delegate
width: ListView.view.width
required property var model
required property bool isDefault
text: model.display
onClicked: delegate.model.checked = checked
checked: delegate.model.checked
Accessible.description: delegate.isDefault ? i18nd("kirigami-addons6", "Default Language") : ""
}
}
}
}
}
}
}
Kirigami.FormGroup {
title: i18nc("@title", "Options")
Kirigami.FormEntry {
contentItem: QQC2.CheckBox {
text: i18nc("@option:check", "Enable autodetection of language")
checked: kcm.settings.autodetectLanguage
onClicked: {
kcm.settings.autodetectLanguage = checked;
}
}
KCM.SettingHighlighter {
highlight: kcm.settings.autodetectLanguage !== kcm.settings.defaultAutodetectLanguage
}
}
Kirigami.FormEntry {
contentItem: QQC2.CheckBox {
text: i18nc("@option:check", "Automatic spell checking enabled by default")
checked: kcm.settings.checkerEnabledByDefault
onCheckedChanged: kcm.settings.checkerEnabledByDefault = checked
}
KCM.SettingHighlighter {
highlight: kcm.settings.checkerEnabledByDefault !== kcm.settings.defaultCheckerEnabledByDefault
}
}
Kirigami.FormEntry {
contentItem: QQC2.CheckBox {
text: i18nc("@option:check", "Skip all uppercase words")
checked: kcm.settings.skipUppercase
onCheckedChanged: kcm.settings.skipUppercase = checked
}
KCM.SettingHighlighter {
highlight: kcm.settings.skipUppercase !== kcm.settings.defaultSkipUppercase
}
}
Kirigami.FormEntry {
contentItem: QQC2.CheckBox {
text: i18nc("@option:check", "Skip run-together words")
checked: kcm.settings.skipRunTogether
onCheckedChanged: kcm.settings.skipRunTogether = checked
}
KCM.SettingHighlighter {
highlight: kcm.settings.skipRunTogether !== kcm.settings.defaultSkipRunTogether
}
}
}
Kirigami.FormGroup {
title: i18nc("@title", "Ignored Words")
Kirigami.FormEntry {
contentItem: QQC2.Button {
icon.name: "view-visible-off"
text: i18nc("@action:button", "Configure Ignored Words…")
onClicked: {
let dialog = ignoredWordsComponent.createObject(root) as Kirigami.Dialog;
dialog.open();
}
}
KCM.SettingHighlighter {
highlight: kcm.settings.currentIgnoreList.length !== kcm.settings.defaultIgnoreList.length || !kcm.settings.currentIgnoreList.every(value => kcm.settings.defaultIgnoreList.includes(value))
}
Component {
id: ignoredWordsComponent
Kirigami.Dialog {
id: dialog
title: i18nc("@title:dialog", "Ignored Words")
width: Kirigami.Units.gridUnit * 24
standardButtons: QQC2.DialogButtonBox.Close
onRejected: dialog.close()
footerLeadingComponent: Kirigami.ActionTextField {
id: newWord
placeholderText: i18nc("@info:placeholder", "Enter a word to ignore")
Layout.fillWidth: true
rightActions: [
Kirigami.Action {
enabled: newWord.text.length > 0
text: i18nc("@action:button", "Add Word")
tooltip: text
icon.name: "list-add"
onTriggered: {
let words = kcm.settings.currentIgnoreList;
words.push(newWord.text);
kcm.settings.currentIgnoreList = words;
}
}
]
}
ListView {
model: kcm.settings.currentIgnoreList
delegate: QQC2.ItemDelegate {
id: delegate
width: ListView.view.width
required property string modelData
contentItem: Kirigami.TitleSubtitleWithActions {
title: delegate.modelData
actions: [
Kirigami.Action {
text: i18nc("@action:button", "Delete")
tooltip: text
icon.name: "list-remove"
displayHint: Kirigami.DisplayHint.IconOnly
onTriggered: {
kcm.settings.currentIgnoreList = kcm.settings.currentIgnoreList.filter((value, _, _) => {
return value !== delegate.modelData;
});
}
}
]
}
}
}
}
}
}
}
}
}