diff --git a/src/qmlcontrols/kcmcontrols/CMakeLists.txt b/src/qmlcontrols/kcmcontrols/CMakeLists.txt --- a/src/qmlcontrols/kcmcontrols/CMakeLists.txt +++ b/src/qmlcontrols/kcmcontrols/CMakeLists.txt @@ -2,6 +2,7 @@ set(kcmcontrols_SRCS kcmcontrolsplugin.cpp + settingstateproxy.cpp ) add_library(kcmcontrolsplugin SHARED ${kcmcontrols_SRCS}) @@ -12,6 +13,7 @@ Qt5::Gui KF5::CoreAddons KF5::QuickAddons + KF5::ConfigCore ) install(TARGETS kcmcontrolsplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kcm) diff --git a/src/qmlcontrols/kcmcontrols/kcmcontrolsplugin.cpp b/src/qmlcontrols/kcmcontrols/kcmcontrolsplugin.cpp --- a/src/qmlcontrols/kcmcontrols/kcmcontrolsplugin.cpp +++ b/src/qmlcontrols/kcmcontrols/kcmcontrolsplugin.cpp @@ -24,12 +24,13 @@ #include - +#include "settingstateproxy.h" void KCMControlsPlugin::registerTypes(const char *uri) { Q_ASSERT(uri == QByteArray("org.kde.kcm")); - qmlRegisterUncreatableType("org.kde.kcm", 1, 0, "ConfigModule", + qmlRegisterUncreatableType(uri, 1, 0, "ConfigModule", QLatin1String("Do not create objects of type ConfigModule")); + qmlRegisterType(uri, 1, 3, "SettingStateProxy"); } diff --git a/src/qmlcontrols/kcmcontrols/qml/SettingStateBinding.qml b/src/qmlcontrols/kcmcontrols/qml/SettingStateBinding.qml new file mode 100644 --- /dev/null +++ b/src/qmlcontrols/kcmcontrols/qml/SettingStateBinding.qml @@ -0,0 +1,111 @@ +/* + Copyright (c) 2020 Kevin Ottens + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +import QtQuick 2.8 +import org.kde.kcm 1.3 as KCM + +/** + * SettingStateBinding automatically impacts the representation + * of an item based on the state of a setting. It will disable + * the item if the setting is immutable and use a visual indicator + * for the state of the setting. + * + * This is a higher level convenience wrapper for KCM.SettingStateProxy + * and KCM.SettingStateIndicator. + * + * @since 5.69 + */ +Item { + id: root + + /** + * target: Item + * The graphical element which state we want to manage based on a setting + */ + property Item target + + /** + * configObject: KCoreConfigSkeleton + * The config object which will be monitored for setting state changes + */ + property alias configObject: settingState.configObject + + /** + * itemName: string + * The name of the item representing the setting in the config object + */ + property alias itemName: settingState.itemName + + /** + * extraEnabledPredicate: bool + * An extra predicate which will be applied to the enabled property + * of the target combined with the immutable state of the setting + */ + property bool extraEnabledPredicate: true + + /** + * indicatorAsOverlay: bool + * If true the state indicator will be displayed inside the boundaries + * of the target item which is convenient for items with lots of empty + * space like list or grid views (default is false) + */ + property bool indicatorAsOverlay: false + + + QtObject { + id: impl + property KCM.SettingStateIndicator indicator: null + // Necessary for proper binding within the Component element, + // otherwise it would get null for its created instances + readonly property KCM.SettingStateProxy settingState: settingState + } + + onTargetChanged: { + if (impl.indicator) { + impl.indicator.destroy() + impl.indicator = null + } + + if (target) { + impl.indicator = indicatorComponent.createObject(target) + } + } + + // We use it via a Component because we potentially need + // to escape the parent/siblings only constraint for anchoring + // (since we anchor to root.target which could be anywhere) + Component { + id: indicatorComponent + KCM.SettingStateIndicator { + anchors.left: root.indicatorAsOverlay ? undefined : root.target.right + anchors.right: root.indicatorAsOverlay ? root.target.right : undefined + anchors.top: root.target.top + settingState: impl.settingState + } + } + + Binding { + target: root.target + property: "enabled" + value: extraEnabledPredicate && !settingState.immutable + } + + KCM.SettingStateProxy { + id: settingState + } +} diff --git a/src/qmlcontrols/kcmcontrols/qml/SettingStateIndicator.qml b/src/qmlcontrols/kcmcontrols/qml/SettingStateIndicator.qml new file mode 100644 --- /dev/null +++ b/src/qmlcontrols/kcmcontrols/qml/SettingStateIndicator.qml @@ -0,0 +1,61 @@ +/* + Copyright (c) 2020 Kevin Ottens + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +import QtQuick 2.8 +import org.kde.kcm 1.3 as KCM +import org.kde.kirigami 2.2 as Kirigami + +/** + * A simple icon which automatically picks an icon, displays or hides itself + * depending on a setting state. + * + * @since 5.69 + */ +Item { + id: root + + /** + * settingState: KCM.SettingStateProxy + * The state used by the indicator to decides if and how it is displayed + */ + property KCM.SettingStateProxy settingState + + width: 16 + height: 16 + visible: icon.source !== "" + + Kirigami.Icon { + id: icon + function iconName() { + if (!root.settingState) { + return "" + } + + var saveNeeded = root.settingState.saveNeeded + var defaulted = root.settingState.defaulted + + return saveNeeded && !defaulted ? "document-save-as" + : saveNeeded ? "document-save" + : !defaulted ? "document-edit" + : "" + } + + anchors.fill: parent + source: iconName() + } +} diff --git a/src/qmlcontrols/kcmcontrols/qml/qmldir b/src/qmlcontrols/kcmcontrols/qml/qmldir --- a/src/qmlcontrols/kcmcontrols/qml/qmldir +++ b/src/qmlcontrols/kcmcontrols/qml/qmldir @@ -7,3 +7,5 @@ ScrollView 1.2 ScrollView.qml ScrollViewKCM 1.2 ScrollViewKCM.qml SimpleKCM 1.1 SimpleKCM.qml +SettingStateIndicator 1.3 SettingStateIndicator.qml +SettingStateBinding 1.3 SettingStateBinding.qml diff --git a/src/qmlcontrols/kcmcontrols/settingstateproxy.h b/src/qmlcontrols/kcmcontrols/settingstateproxy.h new file mode 100644 --- /dev/null +++ b/src/qmlcontrols/kcmcontrols/settingstateproxy.h @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Kevin Ottens + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef SETTINGSTATEPROXY_H +#define SETTINGSTATEPROXY_H + +#include +#include + +#include +/** + * This element allows to represent in a declarative way the + * state of a particular setting in a config object. + * + * @since 5.69 + */ +class SettingStateProxy : public QObject +{ + Q_OBJECT + + /** + * The config object which will be monitored for setting state changes + */ + Q_PROPERTY(KCoreConfigSkeleton* configObject READ configObject WRITE setConfigObject NOTIFY configObjectChanged) + + /** + * The name of the item representing the setting in the config object + */ + Q_PROPERTY(QString itemName READ itemName WRITE setItemName NOTIFY itemNameChanged) + + + /** + * Indicates if the setting is marked as immutable + */ + Q_PROPERTY(bool immutable READ isImmutable NOTIFY immutableChanged) + + + /** + * Indicates if the setting has changed since the last load and needs saving + */ + Q_PROPERTY(bool saveNeeded READ isSaveNeeded NOTIFY saveNeededChanged) + + /** + * Indicates if the setting differs from its default value + */ + Q_PROPERTY(bool defaulted READ isDefaulted NOTIFY defaultedChanged) +public: + using QObject::QObject; + + KCoreConfigSkeleton *configObject() const; + void setConfigObject(KCoreConfigSkeleton *configObject); + + QString itemName() const; + void setItemName(const QString &itemName); + + bool isImmutable() const; + bool isSaveNeeded() const; + bool isDefaulted() const; + +Q_SIGNALS: + void configObjectChanged(); + void itemNameChanged(); + + void immutableChanged(); + void saveNeededChanged(); + void defaultedChanged(); + +private Q_SLOTS: + void updateState(); + +private: + void connectSetting(); + + QPointer m_configObject; + QString m_itemName; + bool m_immutable = false; + bool m_saveNeeded = false; + bool m_defaulted = true; +}; + +#endif diff --git a/src/qmlcontrols/kcmcontrols/settingstateproxy.cpp b/src/qmlcontrols/kcmcontrols/settingstateproxy.cpp new file mode 100644 --- /dev/null +++ b/src/qmlcontrols/kcmcontrols/settingstateproxy.cpp @@ -0,0 +1,146 @@ +/* + * Copyright 2020 Kevin Ottens + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "settingstateproxy.h" + +#include +#include + +KCoreConfigSkeleton *SettingStateProxy::configObject() const +{ + return m_configObject; +} + +void SettingStateProxy::setConfigObject(KCoreConfigSkeleton *configObject) +{ + if (m_configObject == configObject) { + return; + } + + if (m_configObject) { + m_configObject->disconnect(this); + } + + m_configObject = configObject; + emit configObjectChanged(); + updateState(); + connectSetting(); +} + +QString SettingStateProxy::itemName() const +{ + return m_itemName; +} + +void SettingStateProxy::setItemName(const QString &itemName) +{ + if (m_itemName == itemName) { + return; + } + + if (m_configObject) { + m_configObject->disconnect(this); + } + + m_itemName = itemName; + emit itemNameChanged(); + updateState(); + connectSetting(); +} + +bool SettingStateProxy::isImmutable() const +{ + return m_immutable; +} + +bool SettingStateProxy::isSaveNeeded() const +{ + return m_saveNeeded; +} + +bool SettingStateProxy::isDefaulted() const +{ + return m_defaulted; +} + +void SettingStateProxy::updateState() +{ + const auto item = m_configObject ? m_configObject->findItem(m_itemName) : nullptr; + const auto immutable = item ? item->isImmutable() : false; + const auto saveNeeded = item ? item->isSaveNeeded() : false; + const auto defaulted = item ? item->isDefault() : true; + + if (m_immutable != immutable) { + m_immutable = immutable; + emit immutableChanged(); + } + + if (m_saveNeeded != saveNeeded) { + m_saveNeeded = saveNeeded; + emit saveNeededChanged(); + } + + if (m_defaulted != defaulted) { + m_defaulted = defaulted; + emit defaultedChanged(); + } +} + +void SettingStateProxy::connectSetting() +{ + const auto item = m_configObject ? m_configObject->findItem(m_itemName) : nullptr; + if (!item) { + return; + } + + const auto updateStateSlotIndex = metaObject()->indexOfMethod("updateState()"); + Q_ASSERT(updateStateSlotIndex >= 0); + const auto updateStateSlot = metaObject()->method(updateStateSlotIndex); + Q_ASSERT(updateStateSlot.isValid()); + + const auto itemHasSignals = dynamic_cast(item) || dynamic_cast(item); + if (!itemHasSignals) { + qWarning() << "Attempting to use SettingStateProxy with a non signalling item:" << m_itemName; + return; + } + + const auto propertyName = [this] { + auto name = m_itemName; + if (name.at(0).isUpper()) { + name[0] = name[0].toLower(); + } + return name.toUtf8(); + }(); + + const auto metaObject = m_configObject->metaObject(); + const auto propertyIndex = metaObject->indexOfProperty(propertyName.constData()); + Q_ASSERT(propertyIndex >= 0); + const auto property = metaObject->property(propertyIndex); + Q_ASSERT(property.isValid()); + if (!property.hasNotifySignal()) { + qWarning() << "Attempting to use SettingStateProxy with a non notifying property:" << propertyName; + return; + } + + const auto changedSignal = property.notifySignal(); + Q_ASSERT(changedSignal.isValid()); + connect(m_configObject, changedSignal, this, updateStateSlot); + connect(m_configObject, &KCoreConfigSkeleton::configChanged, + this, &SettingStateProxy::updateState); +}