/* This file is part of the KDE libraries SPDX-FileCopyrightText: 2003 Benjamin C Meyer SPDX-FileCopyrightText: 2003 Waldo Bastian SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau SPDX-FileCopyrightText: 2020 Kevin Ottens SPDX-FileCopyrightText: 2020 Cyril Rossi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kconfigdialogmanager.h" #include "kconfigdialogmanager_p.h" #include "kconfigwidgets_debug.h" #include #include #include #include #include #include #include #include #include typedef QHash MyHash; Q_GLOBAL_STATIC(MyHash, s_propertyMap) Q_GLOBAL_STATIC(MyHash, s_changedMap) KConfigDialogManager::KConfigDialogManager(QWidget *parent, KCoreConfigSkeleton *conf) : QObject(parent) , d(new KConfigDialogManagerPrivate(this)) { d->m_conf = conf; d->m_dialog = parent; init(true); } KConfigDialogManager::~KConfigDialogManager() = default; // KF6: Drop this and get signals only from metaObject and/or widget's dynamic properties kcfg_property/kcfg_propertyNotify void KConfigDialogManager::initMaps() { if (s_propertyMap()->isEmpty()) { s_propertyMap()->insert(QStringLiteral("KButtonGroup"), "current"); s_propertyMap()->insert(QStringLiteral("KColorButton"), "color"); s_propertyMap()->insert(QStringLiteral("KColorCombo"), "color"); s_propertyMap()->insert(QStringLiteral("KKeySequenceWidget"), "keySequence"); } if (s_changedMap()->isEmpty()) { // Qt #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) s_changedMap()->insert(QStringLiteral("QCheckBox"), SIGNAL(checkStateChanged(Qt::CheckState))); #else s_changedMap()->insert(QStringLiteral("QCheckBox"), SIGNAL(stateChanged(int))); #endif s_changedMap()->insert(QStringLiteral("QPushButton"), SIGNAL(clicked(bool))); s_changedMap()->insert(QStringLiteral("QRadioButton"), SIGNAL(toggled(bool))); s_changedMap()->insert(QStringLiteral("QGroupBox"), SIGNAL(toggled(bool))); s_changedMap()->insert(QStringLiteral("QComboBox"), SIGNAL(activated(int))); s_changedMap()->insert(QStringLiteral("QDateEdit"), SIGNAL(dateChanged(QDate))); s_changedMap()->insert(QStringLiteral("QTimeEdit"), SIGNAL(timeChanged(QTime))); s_changedMap()->insert(QStringLiteral("QDateTimeEdit"), SIGNAL(dateTimeChanged(QDateTime))); s_changedMap()->insert(QStringLiteral("QDial"), SIGNAL(valueChanged(int))); s_changedMap()->insert(QStringLiteral("QDoubleSpinBox"), SIGNAL(valueChanged(double))); s_changedMap()->insert(QStringLiteral("QLineEdit"), SIGNAL(textChanged(QString))); s_changedMap()->insert(QStringLiteral("QSlider"), SIGNAL(valueChanged(int))); s_changedMap()->insert(QStringLiteral("QSpinBox"), SIGNAL(valueChanged(int))); s_changedMap()->insert(QStringLiteral("QTextEdit"), SIGNAL(textChanged())); s_changedMap()->insert(QStringLiteral("QTextBrowser"), SIGNAL(sourceChanged(QString))); s_changedMap()->insert(QStringLiteral("QPlainTextEdit"), SIGNAL(textChanged())); s_changedMap()->insert(QStringLiteral("QTabWidget"), SIGNAL(currentChanged(int))); // KDE s_changedMap()->insert(QStringLiteral("KComboBox"), SIGNAL(activated(int))); s_changedMap()->insert(QStringLiteral("KFontComboBox"), SIGNAL(activated(int))); s_changedMap()->insert(QStringLiteral("KFontRequester"), SIGNAL(fontSelected(QFont))); s_changedMap()->insert(QStringLiteral("KFontChooser"), SIGNAL(fontSelected(QFont))); s_changedMap()->insert(QStringLiteral("KColorCombo"), SIGNAL(activated(QColor))); s_changedMap()->insert(QStringLiteral("KColorButton"), SIGNAL(changed(QColor))); s_changedMap()->insert(QStringLiteral("KDatePicker"), SIGNAL(dateSelected(QDate))); s_changedMap()->insert(QStringLiteral("KDateWidget"), SIGNAL(changed(QDate))); s_changedMap()->insert(QStringLiteral("KDateTimeWidget"), SIGNAL(valueChanged(QDateTime))); s_changedMap()->insert(QStringLiteral("KEditListWidget"), SIGNAL(changed())); s_changedMap()->insert(QStringLiteral("KListWidget"), SIGNAL(itemSelectionChanged())); s_changedMap()->insert(QStringLiteral("KLineEdit"), SIGNAL(textChanged(QString))); s_changedMap()->insert(QStringLiteral("KRestrictedLine"), SIGNAL(textChanged(QString))); s_changedMap()->insert(QStringLiteral("KTextEdit"), SIGNAL(textChanged())); s_changedMap()->insert(QStringLiteral("KUrlRequester"), SIGNAL(textChanged(QString))); s_changedMap()->insert(QStringLiteral("KUrlComboRequester"), SIGNAL(textChanged(QString))); s_changedMap()->insert(QStringLiteral("KUrlComboBox"), SIGNAL(urlActivated(QUrl))); s_changedMap()->insert(QStringLiteral("KButtonGroup"), SIGNAL(changed(int))); } } QHash *KConfigDialogManager::propertyMap() { initMaps(); return s_propertyMap(); } void KConfigDialogManager::init(bool trackChanges) { initMaps(); d->trackChanges = trackChanges; // Go through all of the children of the widgets and find all known widgets (void)parseChildren(d->m_dialog, trackChanges); } void KConfigDialogManager::addWidget(QWidget *widget) { (void)parseChildren(widget, true); } void KConfigDialogManager::setupWidget(QWidget *widget, KConfigSkeletonItem *item) { QVariant minValue = item->minValue(); if (minValue.isValid()) { // KSelector is using this property if (widget->metaObject()->indexOfProperty("minValue") != -1) { widget->setProperty("minValue", minValue); } if (widget->metaObject()->indexOfProperty("minimum") != -1) { widget->setProperty("minimum", minValue); } } QVariant maxValue = item->maxValue(); if (maxValue.isValid()) { // KSelector is using this property if (widget->metaObject()->indexOfProperty("maxValue") != -1) { widget->setProperty("maxValue", maxValue); } if (widget->metaObject()->indexOfProperty("maximum") != -1) { widget->setProperty("maximum", maxValue); } } if (widget->whatsThis().isEmpty()) { QString whatsThis = item->whatsThis(); if (!whatsThis.isEmpty()) { widget->setWhatsThis(whatsThis); } } if (widget->toolTip().isEmpty()) { QString toolTip = item->toolTip(); if (!toolTip.isEmpty()) { widget->setToolTip(toolTip); } } // If it is a QGroupBox with only autoExclusive buttons // and has no custom property and the config item type // is an integer, assume we want to save the index like we did with // KButtonGroup instead of if it is checked or not QGroupBox *gb = qobject_cast(widget); if (gb && getCustomProperty(gb).isEmpty()) { const KConfigSkeletonItem *item = d->m_conf->findItem(widget->objectName().mid(5)); if (item->property().userType() == QMetaType::Int) { QObjectList children = gb->children(); children.removeAll(gb->layout()); const QList buttons = gb->findChildren(); bool allAutoExclusiveDirectChildren = true; for (QAbstractButton *button : buttons) { allAutoExclusiveDirectChildren = allAutoExclusiveDirectChildren && button->autoExclusive() && button->parent() == gb; } if (allAutoExclusiveDirectChildren) { d->allExclusiveGroupBoxes << widget; } } } if (!item->isEqual(property(widget))) { setProperty(widget, item->property()); } d->updateWidgetIndicator(item->name(), widget); } bool KConfigDialogManager::parseChildren(const QWidget *widget, bool trackChanges) { bool valueChanged = false; const QList listOfChildren = widget->children(); if (listOfChildren.isEmpty()) { //?? XXX return valueChanged; } const QMetaMethod onWidgetModifiedSlot = metaObject()->method(metaObject()->indexOfSlot("onWidgetModified()")); Q_ASSERT(onWidgetModifiedSlot.isValid() && metaObject()->indexOfSlot("onWidgetModified()") >= 0); for (QObject *object : listOfChildren) { if (!object->isWidgetType()) { continue; // Skip non-widgets } QWidget *childWidget = static_cast(object); QString widgetName = childWidget->objectName(); bool bParseChildren = true; bool bSaveInsideGroupBox = d->insideGroupBox; if (widgetName.startsWith(QLatin1String("kcfg_"))) { // This is one of our widgets! QString configId = widgetName.mid(5); KConfigSkeletonItem *item = d->m_conf->findItem(configId); if (item) { d->knownWidget.insert(configId, childWidget); setupWidget(childWidget, item); if (trackChanges) { bool changeSignalFound = false; if (d->allExclusiveGroupBoxes.contains(childWidget)) { const QList buttons = childWidget->findChildren(); for (QAbstractButton *button : buttons) { connect(button, &QAbstractButton::toggled, this, [this] { d->onWidgetModified(); }); } } QByteArray propertyChangeSignal = getCustomPropertyChangedSignal(childWidget); if (propertyChangeSignal.isEmpty()) { propertyChangeSignal = getUserPropertyChangedSignal(childWidget); } if (propertyChangeSignal.isEmpty()) { // get the change signal from the meta object const QMetaObject *metaObject = childWidget->metaObject(); QByteArray userproperty = getCustomProperty(childWidget); if (userproperty.isEmpty()) { userproperty = getUserProperty(childWidget); } if (!userproperty.isEmpty()) { const int indexOfProperty = metaObject->indexOfProperty(userproperty.constData()); if (indexOfProperty != -1) { const QMetaProperty property = metaObject->property(indexOfProperty); const QMetaMethod notifySignal = property.notifySignal(); if (notifySignal.isValid()) { 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.constData(), this, SLOT(onWidgetModified())); changeSignalFound = true; } if (changeSignalFound) { QComboBox *cb = qobject_cast(childWidget); if (cb && cb->isEditable()) { connect(cb, &QComboBox::editTextChanged, this, &KConfigDialogManager::widgetModified); } } } QGroupBox *gb = qobject_cast(childWidget); if (!gb) { bParseChildren = false; } else { d->insideGroupBox = true; } } else { qCWarning(KCONFIG_WIDGETS_LOG) << "A widget named" << widgetName << "was found but there is no setting named" << configId; } } else if (QLabel *label = qobject_cast(childWidget)) { QWidget *buddy = label->buddy(); if (!buddy) { continue; } QString buddyName = buddy->objectName(); if (buddyName.startsWith(QLatin1String("kcfg_"))) { // This is one of our widgets! QString configId = buddyName.mid(5); d->buddyWidget.insert(configId, childWidget); } } // kf5: commented out to reduce debug output // #ifndef NDEBUG // else if (!widgetName.isEmpty() && trackChanges) // { // QHash::const_iterator changedIt = s_changedMap()->constFind(childWidget->metaObject()->className()); // if (changedIt != s_changedMap()->constEnd()) // { // if ((!d->insideGroupBox || !qobject_cast(childWidget)) && // !qobject_cast(childWidget) &&!qobject_cast(childWidget) ) // qCDebug(KCONFIG_WIDGETS_LOG) << "Widget '" << widgetName << "' (" << childWidget->metaObject()->className() << ") remains unmanaged."; // } // } // #endif if (bParseChildren) { // this widget is not known as something we can store. // Maybe we can store one of its children. valueChanged |= parseChildren(childWidget, trackChanges); } d->insideGroupBox = bSaveInsideGroupBox; } return valueChanged; } void KConfigDialogManager::updateWidgets() { bool changed = false; bool bSignalsBlocked = signalsBlocked(); blockSignals(true); QWidget *widget; QHashIterator it(d->knownWidget); while (it.hasNext()) { it.next(); widget = it.value(); KConfigSkeletonItem *item = d->m_conf->findItem(it.key()); if (!item) { qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!"; continue; } if (!item->isEqual(property(widget))) { setProperty(widget, item->property()); // qCDebug(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "[" << widget->className() << "] has changed"; changed = true; } if (item->isImmutable()) { widget->setEnabled(false); QWidget *buddy = d->buddyWidget.value(it.key(), nullptr); if (buddy) { buddy->setEnabled(false); } } } blockSignals(bSignalsBlocked); 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::setDefaultsIndicatorsVisible(bool enabled) { d->setDefaultsIndicatorsVisible(enabled); } void KConfigDialogManager::updateSettings() { bool changed = false; QWidget *widget; QHashIterator it(d->knownWidget); while (it.hasNext()) { it.next(); widget = it.value(); KConfigSkeletonItem *item = d->m_conf->findItem(it.key()); if (!item) { qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!"; continue; } QVariant fromWidget = property(widget); if (!item->isEqual(fromWidget)) { item->setProperty(fromWidget); changed = true; } } if (changed) { d->m_conf->save(); Q_EMIT settingsChanged(); d->updateAllWidgetIndicators(); } } QByteArray KConfigDialogManager::getUserProperty(const QWidget *widget) const { MyHash *map = s_propertyMap(); const QMetaObject *metaObject = widget->metaObject(); const QString className(QLatin1String(metaObject->className())); auto it = map->find(className); if (it == map->end()) { const QMetaProperty userProp = metaObject->userProperty(); if (userProp.isValid()) { it = map->insert(className, userProp.name()); // qCDebug(KCONFIG_WIDGETS_LOG) << "class name: '" << className //<< " 's USER property: " << metaProperty.name() << endl; } else { return QByteArray(); // no USER property } } const QComboBox *cb = qobject_cast(widget); if (cb) { const char *qcomboUserPropertyName = cb->QComboBox::metaObject()->userProperty().name(); const int qcomboUserPropertyIndex = qcomboUserPropertyName ? cb->QComboBox::metaObject()->indexOfProperty(qcomboUserPropertyName) : -1; const char *widgetUserPropertyName = metaObject->userProperty().name(); const int widgetUserPropertyIndex = widgetUserPropertyName ? cb->metaObject()->indexOfProperty(widgetUserPropertyName) : -1; // no custom user property set on subclass of QComboBox? if (qcomboUserPropertyIndex == widgetUserPropertyIndex) { return QByteArray(); // use the q/kcombobox special code } } return it != map->end() ? it.value() : QByteArray{}; } QByteArray KConfigDialogManager::getCustomProperty(const QWidget *widget) const { QVariant prop(widget->property("kcfg_property")); if (prop.isValid()) { if (!prop.canConvert()) { qCWarning(KCONFIG_WIDGETS_LOG) << "kcfg_property on" << widget->metaObject()->className() << "is not of type ByteArray"; } else { return prop.toByteArray(); } } return QByteArray(); } QByteArray KConfigDialogManager::getUserPropertyChangedSignal(const QWidget *widget) const { const QString className = QLatin1String(widget->metaObject()->className()); auto changedIt = s_changedMap()->constFind(className); if (changedIt == s_changedMap()->constEnd()) { // If the class name of the widget wasn't in the monitored widgets map, then look for // it again using the super class name. This fixes a problem with using QtRuby/Korundum // widgets with KConfigXT where 'Qt::Widget' wasn't being seen a the real deal, even // though it was a 'QWidget'. if (widget->metaObject()->superClass()) { const QString parentClassName = QLatin1String(widget->metaObject()->superClass()->className()); changedIt = s_changedMap()->constFind(parentClassName); } } return (changedIt == s_changedMap()->constEnd()) ? QByteArray() : *changedIt; } QByteArray KConfigDialogManager::getCustomPropertyChangedSignal(const QWidget *widget) const { QVariant prop(widget->property("kcfg_propertyNotify")); if (prop.isValid()) { if (!prop.canConvert()) { qCWarning(KCONFIG_WIDGETS_LOG) << "kcfg_propertyNotify on" << widget->metaObject()->className() << "is not of type ByteArray"; } else { return prop.toByteArray(); } } return QByteArray(); } void KConfigDialogManager::setProperty(QWidget *w, const QVariant &v) { if (d->allExclusiveGroupBoxes.contains(w)) { const QList buttons = w->findChildren(); if (v.toInt() < buttons.count()) { buttons[v.toInt()]->setChecked(true); } return; } QByteArray userproperty = getCustomProperty(w); if (userproperty.isEmpty()) { userproperty = getUserProperty(w); } if (userproperty.isEmpty()) { QComboBox *cb = qobject_cast(w); if (cb) { if (cb->isEditable()) { int i = cb->findText(v.toString()); if (i != -1) { cb->setCurrentIndex(i); } else { cb->setEditText(v.toString()); } } else { cb->setCurrentIndex(v.toInt()); } return; } } if (userproperty.isEmpty()) { qCWarning(KCONFIG_WIDGETS_LOG) << w->metaObject()->className() << "widget not handled!"; return; } w->setProperty(userproperty.constData(), v); } QVariant KConfigDialogManager::property(QWidget *w) const { if (d->allExclusiveGroupBoxes.contains(w)) { const QList buttons = w->findChildren(); for (int i = 0; i < buttons.count(); ++i) { if (buttons[i]->isChecked()) { return i; } } return -1; } QByteArray userproperty = getCustomProperty(w); if (userproperty.isEmpty()) { userproperty = getUserProperty(w); } if (userproperty.isEmpty()) { QComboBox *cb = qobject_cast(w); if (cb) { if (cb->isEditable()) { return QVariant(cb->currentText()); } else { return QVariant(cb->currentIndex()); } } } if (userproperty.isEmpty()) { qCWarning(KCONFIG_WIDGETS_LOG) << w->metaObject()->className() << "widget not handled!"; return QVariant(); } return w->property(userproperty.constData()); } bool KConfigDialogManager::hasChanged() const { QWidget *widget; QHashIterator it(d->knownWidget); while (it.hasNext()) { it.next(); widget = it.value(); KConfigSkeletonItem *item = d->m_conf->findItem(it.key()); if (!item) { qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!"; continue; } if (!item->isEqual(property(widget))) { // qCDebug(KCONFIG_WIDGETS_LOG) << "Widget for '" << it.key() << "' has changed."; return true; } } return false; } bool KConfigDialogManager::isDefault() const { QWidget *widget; QHashIterator it(d->knownWidget); while (it.hasNext()) { it.next(); widget = it.value(); KConfigSkeletonItem *item = d->m_conf->findItem(it.key()); if (!item) { qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!"; continue; } if (property(widget) != item->getDefault()) { return false; } } return true; } KConfigDialogManagerPrivate::KConfigDialogManagerPrivate(KConfigDialogManager *qq) : q(qq) , insideGroupBox(false) , defaultsIndicatorsVisible(false) { } void KConfigDialogManagerPrivate::setDefaultsIndicatorsVisible(bool enabled) { if (defaultsIndicatorsVisible != enabled) { defaultsIndicatorsVisible = enabled; updateAllWidgetIndicators(); } } void KConfigDialogManagerPrivate::onWidgetModified() { const auto widget = qobject_cast(q->sender()); Q_ASSERT(widget); const QLatin1String prefix("kcfg_"); QString configId = widget->objectName(); if (configId.startsWith(prefix)) { configId.remove(0, prefix.size()); updateWidgetIndicator(configId, widget); } else { auto *parent = qobject_cast(widget->parent()); Q_ASSERT(parent); configId = parent->objectName(); Q_ASSERT(configId.startsWith(prefix)); configId.remove(0, prefix.size()); updateWidgetIndicator(configId, parent); } Q_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->getDefault(); const auto defaulted = widgetValue == defaultValue; if (allExclusiveGroupBoxes.contains(widget)) { const QList buttons = widget->findChildren(); for (int i = 0; i < buttons.count(); i++) { const auto value = widgetValue.toInt() == i && !defaulted && defaultsIndicatorsVisible; buttons.at(i)->setProperty("_kde_highlight_neutral", value); buttons.at(i)->update(); } } else { widget->setProperty("_kde_highlight_neutral", !defaulted && defaultsIndicatorsVisible); widget->update(); } } void KConfigDialogManagerPrivate::updateAllWidgetIndicators() { QHashIterator it(knownWidget); while (it.hasNext()) { it.next(); updateWidgetIndicator(it.key(), it.value()); } } #include "moc_kconfigdialogmanager.cpp"