diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,7 @@ krecentfilesaction.cpp kstandardaction.cpp ktipdialog.cpp + settingsstatusindicator.cpp ) ecm_qt_declare_logging_category(kconfigwidgets_SRCS HEADER kconfigwidgets_debug.h diff --git a/src/kconfigdialogmanager.h b/src/kconfigdialogmanager.h --- a/src/kconfigdialogmanager.h +++ b/src/kconfigdialogmanager.h @@ -381,8 +381,10 @@ * KConfigDialogManager KConfigDialogManagerPrivate class. */ KConfigDialogManagerPrivate *const d; + friend class KConfigDialogManagerPrivate; Q_DISABLE_COPY(KConfigDialogManager) + Q_PRIVATE_SLOT(d, void onWidgetModified()) }; #endif // KCONFIGDIALOGMANAGER_H diff --git a/src/kconfigdialogmanager.cpp b/src/kconfigdialogmanager.cpp --- a/src/kconfigdialogmanager.cpp +++ b/src/kconfigdialogmanager.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2003 Benjamin C Meyer (ben+kdelibs at meyerhome dot net) * Copyright (C) 2003 Waldo Bastian * Copyright (C) 2017 Friedrich W. H. Kossebau + * 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 @@ -21,8 +22,11 @@ */ #include "kconfigdialogmanager.h" +#include "kconfigdialogmanager_p.h" #include "kconfigwidgets_debug.h" +#include "settingsstatusindicator_p.h" + #include #include #include @@ -38,32 +42,6 @@ Q_GLOBAL_STATIC(MyHash, s_propertyMap) Q_GLOBAL_STATIC(MyHash, s_changedMap) -class KConfigDialogManagerPrivate -{ - -public: - KConfigDialogManagerPrivate(KConfigDialogManager *q) : q(q), insideGroupBox(false) { } - -public: - KConfigDialogManager * const q; - - /** - * KConfigSkeleton object used to store settings - */ - KCoreConfigSkeleton *m_conf = nullptr; - - /** - * Dialog being managed - */ - QWidget *m_dialog = nullptr; - - QHash knownWidget; - QHash buddyWidget; - QSet allExclusiveGroupBoxes; - bool insideGroupBox : 1; - bool trackChanges : 1; -}; - KConfigDialogManager::KConfigDialogManager(QWidget *parent, KCoreConfigSkeleton *conf) : QObject(parent), d(new KConfigDialogManagerPrivate(this)) { @@ -226,6 +204,8 @@ if (!item->isEqual(property(widget))) { setProperty(widget, item->property()); } + + d->updateWidgetIndicator(item->name(), widget); } bool KConfigDialogManager::parseChildren(const QWidget *widget, bool trackChanges) @@ -236,8 +216,8 @@ return valueChanged; } - const QMetaMethod widgetModifiedSignal = metaObject()->method(metaObject()->indexOfSignal("widgetModified()")); - Q_ASSERT(widgetModifiedSignal.isValid() && metaObject()->indexOfSignal("widgetModified()")>=0); + const QMetaMethod onWidgetModifiedSlot = metaObject()->method(metaObject()->indexOfSlot("onWidgetModified()")); + Q_ASSERT(onWidgetModifiedSlot.isValid() && metaObject()->indexOfSlot("onWidgetModified()")>=0); for (QObject *object : listOfChildren) { if (!object->isWidgetType()) { @@ -266,7 +246,7 @@ const QList buttons = childWidget->findChildren(); for (QAbstractButton *button : buttons) { connect(button, &QAbstractButton::toggled, - this, &KConfigDialogManager::widgetModified); + this, [this] { d->onWidgetModified(); }); } } @@ -288,16 +268,16 @@ const QMetaProperty property = metaObject->property(indexOfProperty); const QMetaMethod notifySignal = property.notifySignal(); if (notifySignal.isValid()) { - connect(childWidget, notifySignal, this, widgetModifiedSignal); + connect(childWidget, notifySignal, this, onWidgetModifiedSlot); changeSignalFound = true; } } } else { qCWarning(KCONFIG_WIDGETS_LOG) << "Don't know how to monitor widget '" << childWidget->metaObject()->className() << "' for changes!"; } } else { connect(childWidget, propertyChangeSignal, - this, SIGNAL(widgetModified())); + this, SLOT(onWidgetModified())); changeSignalFound = true; } @@ -388,14 +368,16 @@ if (changed) { QTimer::singleShot(0, this, &KConfigDialogManager::widgetModified); + d->updateAllWidgetIndicators(); } } void KConfigDialogManager::updateWidgetsDefault() { bool bUseDefaults = d->m_conf->useDefaults(true); updateWidgets(); d->m_conf->useDefaults(bUseDefaults); + d->updateAllWidgetIndicators(); } void KConfigDialogManager::updateSettings() @@ -423,6 +405,7 @@ if (changed) { d->m_conf->save(); emit settingsChanged(); + d->updateAllWidgetIndicators(); } } @@ -601,3 +584,62 @@ return result; } +KConfigDialogManagerPrivate::KConfigDialogManagerPrivate(KConfigDialogManager *q) + : q(q) + , insideGroupBox(false) +{ +} + +void KConfigDialogManagerPrivate::onWidgetModified() +{ + const auto widget = qobject_cast(q->sender()); + Q_ASSERT(widget && widget->objectName().startsWith("kcfg_")); + const auto configId = widget->objectName().mid(5); + updateWidgetIndicator(configId, widget); + emit q->widgetModified(); +} + +void KConfigDialogManagerPrivate::updateWidgetIndicator(const QString &configId, QWidget *widget) +{ + const auto item = m_conf->findItem(configId); + Q_ASSERT(item); + + const auto widgetValue = q->property(widget); + const auto defaultValue = [item] { + item->swapDefault(); + const auto value = item->property(); + item->swapDefault(); + return value; + }(); + + auto indicator = indicatorWidgets.value(configId); + const auto defaulted = widgetValue == defaultValue; + if (defaulted) { + delete indicator; + indicatorWidgets.remove(configId); + } else { + if (!indicator) { + indicator = new SettingStatusIndicator(widget->parentWidget()); + indicator->setTrackedWidget(widget); + QObject::connect(indicator, &SettingStatusIndicator::clicked, widget, [=] { + q->setProperty(widget, defaultValue); + updateWidgetIndicator(configId, widget); + emit q->widgetModified(); + }); + indicatorWidgets.insert(configId, indicator); + } + Q_ASSERT(indicator); + indicator->setDefaulted(defaulted); + } +} + +void KConfigDialogManagerPrivate::updateAllWidgetIndicators() +{ + QHashIterator it(knownWidget); + while (it.hasNext()) { + it.next(); + updateWidgetIndicator(it.key(), it.value()); + } +} + +#include "moc_kconfigdialogmanager.cpp" diff --git a/src/kconfigdialogmanager_p.h b/src/kconfigdialogmanager_p.h new file mode 100644 --- /dev/null +++ b/src/kconfigdialogmanager_p.h @@ -0,0 +1,66 @@ +/* + * This file is part of the KDE libraries + * Copyright (C) 2003 Benjamin C Meyer (ben+kdelibs at meyerhome dot net) + * Copyright (C) 2003 Waldo Bastian + * Copyright (C) 2017 Friedrich W. H. Kossebau + * 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 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 KCONFIGDIALOGMANAGER_P_H +#define KCONFIGDIALOGMANAGER_P_H + +#include +#include +#include + +class QWidget; +class KConfigDialogManager; +class KCoreConfigSkeleton; +class SettingStatusIndicator; + +class KConfigDialogManagerPrivate +{ +public: + KConfigDialogManagerPrivate(KConfigDialogManager *q); + + void onWidgetModified(); + void updateWidgetIndicator(const QString &configId, QWidget *widget); + void updateAllWidgetIndicators(); + +public: + KConfigDialogManager * const q; + + /** + * KConfigSkeleton object used to store settings + */ + KCoreConfigSkeleton *m_conf = nullptr; + + /** + * Dialog being managed + */ + QWidget *m_dialog = nullptr; + + QHash knownWidget; + QHash buddyWidget; + QHash indicatorWidgets; + QSet allExclusiveGroupBoxes; + bool insideGroupBox : 1; + bool trackChanges : 1; +}; + +#endif // KCONFIGDIALOGMANAGER_P_H diff --git a/src/settingsstatusindicator.cpp b/src/settingsstatusindicator.cpp new file mode 100644 --- /dev/null +++ b/src/settingsstatusindicator.cpp @@ -0,0 +1,206 @@ +/* + * This file is part of the KDE libraries + * 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 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 "settingsstatusindicator_p.h" + +#include +#include +#include +#include +#include + +namespace { +constexpr int INDICATOR_ICON_SIZE = 16; +constexpr const char *originalWidthProperty = "originalWidth"; + +bool isWidgetAtParentEdge(QWidget *widget) +{ + const auto right = widget->pos().x() + widget->width(); + const auto parentWidth = widget->parentWidget()->width(); + return right == parentWidth; +} + +int widgetForcedWidth(QWidget *widget) +{ + // We actually respect the minimum size hint to avoid the widget looking odd + // worst case scenario the indicator isn't visible if the widget is ending at the very + // edge of the window and is already too small to be resized, it's a very unlikely case + // though, when reaching the edge it's often greedy in space anyway + return qMax(widget->width() - INDICATOR_ICON_SIZE, widget->minimumSizeHint().width()); +} + +int widgetExpectedWidth(QWidget *widget) +{ + if (isWidgetAtParentEdge(widget)) { + return widgetForcedWidth(widget); + } else { + return widget->width(); + } +} + +} + +SettingStatusIndicator::SettingStatusIndicator(QWidget *parent) + : QWidget(parent) +{ + show(); +} + +SettingStatusIndicator::~SettingStatusIndicator() +{ + setTrackedWidget(nullptr); // This will neatly reset the width offset +} + +void SettingStatusIndicator::setTrackedWidget(QWidget *widget) +{ + if (m_trackedWidget == widget) { + return; + } + + if (m_trackedWidget) { + resetWidthOffset(); + m_trackedWidget->removeEventFilter(this); + } + + m_trackedWidget = widget; + if (!m_trackedWidget) { + return; + } + + forceWidthOffset(); + applyLayout(); + setVisible(m_trackedWidget->isVisible()); + m_trackedWidget->installEventFilter(this); +} + +bool SettingStatusIndicator::eventFilter(QObject *watched, QEvent *event) +{ + Q_UNUSED(watched) + if (event->type() == QEvent::Resize && !m_forcingSize) { + if (forceWidthOffset()) { + return true; + } + } + + switch (event->type()) { + case QEvent::Resize: + case QEvent::Move: + applyLayout(); + break; + case QEvent::Show: + show(); + break; + case QEvent::Hide: + hide(); + break; + default: + break; + } + + return false; +} + +void SettingStatusIndicator::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event) + + emit clicked(); +} + +void SettingStatusIndicator::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + const auto icon = !isDefaulted() ? QIcon::fromTheme("edit-reset") : QIcon(); + + if (!icon.isNull()) { + QPainter p(this); + auto pos = QPoint(width() - height(), 0); + p.drawPixmap(pos, icon.pixmap(height(), height())); + } +} + +bool SettingStatusIndicator::forceWidthOffset() +{ + // Force resizing only when necessary + if (!isWidgetAtParentEdge(m_trackedWidget)) { + return false; + } + + m_forcingSize = true; + m_trackedWidget->setProperty(originalWidthProperty, m_trackedWidget->width()); + m_trackedWidget->resize(widgetForcedWidth(m_trackedWidget), m_trackedWidget->height()); + m_forcingSize = false; + return true; +} + +void SettingStatusIndicator::resetWidthOffset() +{ + const auto originalWidth = m_trackedWidget->property(originalWidthProperty); + if (originalWidth.isValid()) { + m_forcingSize = true; + auto size = m_trackedWidget->size(); + size.setWidth(originalWidth.toInt()); + m_trackedWidget->setProperty(originalWidthProperty, QVariant()); + m_trackedWidget->resize(size); + m_forcingSize = false; + } +} + +void SettingStatusIndicator::applyLayout() +{ + resize(INDICATOR_ICON_SIZE, INDICATOR_ICON_SIZE); + auto x = m_trackedWidget->pos().x() + m_trackedWidget->width(); + const auto y = m_trackedWidget->pos().y() + (m_trackedWidget->height() - height()) / 2.0f; + + // We didn't get resized which means we're not at the border of the window + // then we try to vertically line up with other indicators + const auto originalWidth = m_trackedWidget->property(originalWidthProperty); + if (!originalWidth.isValid() || m_trackedWidget->width() == originalWidth.toInt()) { + const auto re = QRegularExpression("^kcfg_"); + const auto children = m_trackedWidget->parentWidget()->findChildren(re, Qt::FindDirectChildrenOnly); + const auto rightValues = [=] { + auto result = QVector(); + result.reserve(children.size()); + std::transform(children.cbegin(), children.cend(), + std::back_inserter(result), + [](QWidget *w) { return w->pos().x() + widgetExpectedWidth(w); }); + return result; + }(); + x = *std::max_element(rightValues.cbegin(), rightValues.cend()); + } + + move(x, y); +} + +bool SettingStatusIndicator::isDefaulted() const +{ + return m_defaulted; +} + +void SettingStatusIndicator::setDefaulted(bool defaulted) +{ + if (m_defaulted == defaulted) { + return; + } + + m_defaulted = defaulted; + update(); +} diff --git a/src/settingsstatusindicator_p.h b/src/settingsstatusindicator_p.h new file mode 100644 --- /dev/null +++ b/src/settingsstatusindicator_p.h @@ -0,0 +1,60 @@ +/* + * This file is part of the KDE libraries + * 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 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 SETTINGSSTATUSINDICATOR_P_H +#define SETTINGSSTATUSINDICATOR_P_H + +#include +#include + +class SettingStatusIndicator : public QWidget +{ + Q_OBJECT + Q_PROPERTY(bool defaulted READ isDefaulted WRITE setDefaulted) +public: + explicit SettingStatusIndicator(QWidget *parent); + ~SettingStatusIndicator() override; + + void setTrackedWidget(QWidget *widget); + + bool isDefaulted() const; + +public Q_SLOTS: + void setDefaulted(bool defaulted); + +Q_SIGNALS: + void clicked(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + bool forceWidthOffset(); + void resetWidthOffset(); + void applyLayout(); + + QPointer m_trackedWidget; + bool m_forcingSize = false; + bool m_defaulted = false; +}; + +#endif // SETTINGSSTATUSINDICATOR_P_H