diff --git a/autotests/kconfigdialog_unittest.cpp b/autotests/kconfigdialog_unittest.cpp --- a/autotests/kconfigdialog_unittest.cpp +++ b/autotests/kconfigdialog_unittest.cpp @@ -71,6 +71,25 @@ QString m_other; }; +class TextEditNoUserPropertyNoNotifyWidget : public QWidget +{ + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText) + Q_PROPERTY(QString other READ other WRITE setOther NOTIFY otherChanged USER true) +public: + TextEditNoUserPropertyNoNotifyWidget(QWidget *parent = nullptr) : QWidget(parent) {} + void setText(const QString &text) { m_text = text; emit textChanged(m_text); } + QString text() const { return m_text; } + void setOther(const QString &other) { m_other = other; emit textChanged(m_other); } + QString other() const { return m_other; } +Q_SIGNALS: + void textChanged(const QString &text); + void otherChanged(const QString &other); +private: + QString m_text; + QString m_other; +}; + class ComboBoxPage : public QWidget { public: @@ -208,6 +227,9 @@ void testKConfigCompilerSignalsWithUserProperty() { + // make sure there is nothing registered for the property + KConfigDialogManager::propertyMap()->remove("TextEditUserPropertyWidget"); + KConfigDialogManager::changedMap()->insert("TextEditUserPropertyWidget", SIGNAL(textChanged(QString))); TextEditUserPropertyWidget *edit = new TextEditUserPropertyWidget; @@ -227,14 +249,66 @@ void testKConfigCompilerSignalsWithoutUserPropertyByProperty() { + // make sure there is nothing registered for the property + KConfigDialogManager::propertyMap()->remove("TextEditNoUserPropertyWidget"); + KConfigDialogManager::changedMap()->insert("TextEditNoUserPropertyWidget", SIGNAL(textChanged(QString))); TextEditNoUserPropertyWidget *edit = new TextEditNoUserPropertyWidget; edit->setProperty("kcfg_property", QByteArray("text")); testKConfigCompilerSignals(edit, QStringLiteral("settings5")); } + void testKConfigCompilerSignalsWithUserPropertyAutoSignal() + { + // make sure there is nothing registered + KConfigDialogManager::changedMap()->remove("TextEditUserPropertyWidget"); + KConfigDialogManager::propertyMap()->remove("TextEditUserPropertyWidget"); + + TextEditUserPropertyWidget *edit = new TextEditUserPropertyWidget; + + testKConfigCompilerSignals(edit, QStringLiteral("settings6")); + } + + void testKConfigCompilerSignalsWithoutUserPropertyByMapAutoSignal() + { + // make sure there is nothing registered for the signal + KConfigDialogManager::changedMap()->remove("TextEditNoUserPropertyWidget"); + + KConfigDialogManager::propertyMap()->insert("TextEditNoUserPropertyWidget", QByteArray("text")); + + TextEditNoUserPropertyWidget *edit = new TextEditNoUserPropertyWidget; + + testKConfigCompilerSignals(edit, QStringLiteral("settings7")); + } + + void testKConfigCompilerSignalsWithoutUserPropertyByPropertyAutoSignal() + { + // make sure there is no signal registered + KConfigDialogManager::changedMap()->remove("TextEditNoUserPropertyWidget"); + // next to USER on "other" property, this one should also be ignored + KConfigDialogManager::propertyMap()->insert("TextEditNoUserPropertyWidget", QByteArray("other")); + + TextEditNoUserPropertyWidget *edit = new TextEditNoUserPropertyWidget; + edit->setProperty("kcfg_property", QByteArray("text")); + + testKConfigCompilerSignals(edit, QStringLiteral("settings8")); + } + + void testKConfigCompilerSignalsWithoutUserPropertyByPropertyBySignal() + { + // next to USER being on "other" property, this one should also be ignored + KConfigDialogManager::changedMap()->insert("TextEditNoUserPropertyNoNotifyWidget", SIGNAL(otherChanged(QString))); + KConfigDialogManager::propertyMap()->insert("TextEditNoUserPropertyNoNotifyWidget", QByteArray("other")); + + TextEditNoUserPropertyNoNotifyWidget *edit = new TextEditNoUserPropertyNoNotifyWidget; + edit->setProperty("kcfg_property", QByteArray("text")); + edit->setProperty("kcfg_propertyNotify", SIGNAL(textChanged(QString))); + + testKConfigCompilerSignals(edit, QStringLiteral("settings9")); + } + private: template void testKConfigCompilerSignals(T* edit, const QString& configDialogTitle) diff --git a/src/kconfigdialogmanager.h b/src/kconfigdialogmanager.h --- a/src/kconfigdialogmanager.h +++ b/src/kconfigdialogmanager.h @@ -42,43 +42,139 @@ * (settings were saved) or modified (the user changes a checkbox * from on to off). * - * The names of the widgets to be managed have to correspond to the names of the + * The object names of the widgets to be managed have to correspond to the names of the * configuration entries in the KConfigSkeleton object plus an additional - * "kcfg_" prefix. For example a widget named "kcfg_MyOption" would be - * associated to the configuration entry "MyOption". + * "kcfg_" prefix. For example a widget with the object name "kcfg_MyOption" + * would be associated to the configuration entry "MyOption". * - * New widgets can be added to the map using the static functions propertyMap() and - * changedMap(). Note that you can't just add any class. The class must have a - * matching Q_PROPERTY(...) macro defined, and a signal which is emitted when the - * property changed. Note: by default, the property which is defined as "USER true" - * is used. + * The widget classes of Qt and KDE Frameworks are supported out of the box. * - * For example (note that KColorButton is already added and it doesn't need to - * manually added): + * Custom widget classes are supported if they have a Q_PROPERTY defined for the + * property representing the value edited by the widget. By default the property + * is used for which "USER true" is set. For using another property, see below. * - * kcolorbutton.h defines the following property: + * Example: + * + * A class ColorEditWidget is used in the settings UI to select a color. The + * color value is set and read as type QColor. For that it has a definition of + * the value property similar to this: + * \code + * Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged USER true) + * \endcode + * And of course it has the definition and implementation of the respective + * read & write methods and the notify signal. + * + * To use a widget's property that is not the USER property, the property to use + * can be defined by registering it for the class in the + * KConfigDialogManager::propertyMap(). + * Note: setting the property in the propertyMap affects any instances of that + * widget class in the current application, so use it with care. + * + * Example: + * + * If the ColorEditWidget has another property redColor defined by + * \code + * Q_PROPERTY(int redColorPart READ redColorPart WRITE setRedColorPart NOTIFY redColorPartChanged) + * \endcode + * and this one should be used in the settings, call somewhere in the code before + * using the settings: * \code - * Q_PROPERTY( QColor color READ color WRITE setColor USER true ) + * KConfigDialogManager::propertyMap()->insert("ColorEditWidget", QByteArray("redColorPart")); * \endcode - * and signal: + * + * Alternatively a non-USER property can be selected by setting onto the widget + * instance a property with the key "kcfg_property" and as the value the name of + * the property: * \code - * void changed( const QColor &newColor ); + * ColorEditWidget *myWidget = new ColorEditWidget; + * myWidget->setProperty("kcfg_property", QByteArray("redColorPart")); * \endcode + * When using a UI file, the "kcfg_property" property can also be set using Qt Designer. + * Other than with KConfigDialogManager::propertyMap() this selection of the property + * to use is just valid for this widget instance, and also has priority over the other. * - * To add KColorButton the following code would be inserted in the main: + * If some non-default signal should be used, e.g. because the property to use does not + * have a NOTIFY setting, for a given widget instance the signal to use can be set + * by a property with the key "kcfg_propertyNotify" and as the value the signal signature. + * This will take priority over the signal noted by NOTIFY for the chosen property + * as well as the content of KConfigDialogManager::changedMap(). Since 5.32. + * + * Example: * + * If for a class OtherColorEditWidget there was no NOTIFY set on the USER property, + * but some signal colorSelected(QColor) defined which would be good enough to reflect + * the settings change, defined by + * \code + * Q_PROPERTY(QColor color READ color WRITE setColor USER true) + * Q_SIGNALS: + * void colorSelected(const QColor &color); + * \endcode + * the signal to use would be defined by this: * \code - * KConfigDialogManager::changedMap()->insert("KColorButton", SIGNAL(changed(const QColor &))); + * OtherColorEditWidget *myWidget = new OtherColorEditWidget; + * myWidget->setProperty("kcfg_propertyNotify", SIGNAL(colorSelected(QColor))); * \endcode * - * If you want to use a widget's property that is not the USER property, - * you can define which property to use in the widget's kcfg_property: + * Before version 5.32 of KDE Frameworks, the signal notifying about a change + * of the property value in the widget had to be manually registered for any + * custom widget, using KConfigDialogManager::changedMap(). The same also had + * to be done for custom signals with widgets from Qt and KDE Frameworks. + * So for code which needs to also work with older versions of the KDE Frameworks, + * this still needs to be done. + * Starting with version 5.32, where the new signal handling is effective, the + * signal registered via KConfigDialogManager::changedMap() will take precedence over + * the one read from the Q_PROPERTY declaration, but is overridden for a given + * widget instance by the "kcfg_propertyNotify" property. + * + * Examples: + * + * For the class ColorEditWidget from the previous example this will register + * the change signal as needed: * \code - * KUrlRequester *myWidget = new KUrlRequester; - * myWidget->setProperty("kcfg_property", QByteArray("text")); + * KConfigDialogManager::changedMap()->insert("ColorEditWidget", SIGNAL(colorChanged(QColor))); + * \endcode + * For KDE Framework versions starting with 5.32 this will override then the signal + * as read from the USER property, but as it is the same signal, nothing will break. + * + * If wants wants to reduce conflicts and also only add code to the build as needed, + * one would add both a buildtime switch and a runtime switch like + * \code + * #include + * #include + * // [...] + * #if KCONFIGWIDGETS_VERSION < QT_VERSION_CHECK(5,32,0) + * if (KCoreAddons::version() < QT_VERSION_CHECK(5,32,0)) { + * KConfigDialogManager::changedMap()->insert("ColorEditWidget", SIGNAL(colorChanged(QColor))); + * } + * #endif + * \endcode + * so support for the old variant would be only used when running against an older + * KDE Frameworks, and this again only built in if also compiled against an older version. + * Note: KCoreAddons::version() needs at least KF 5.20 though. + * + * For the class OtherColorEditWidget from the previous example for also older + * KF versions the change signal would be registered by this: + * \code + * KConfigDialogManager::changedMap()->insert("OtherColorEditWidget", SIGNAL(colorSelected(QColor))); + * OtherColorEditWidget *myWidget = new OtherColorEditWidget; + * myWidget->setProperty("kcfg_propertyNotify", SIGNAL(colorSelected(QColor))); + * \endcode + * For KDE Framework versions before 5.32 the "kcfg_propertyNotify" property would + * be ignored and the signal taken from KConfigDialogManager::changedMap(), while + * for newer versions it is taken from that property, which overrides the latter. + * + * Again, using KConfigDialogManager::changedMap could be made to depend on the version, + * so for newer versions any global conflicts are avoided: + * \code + * #include + * #include + * // [...] + * #if KCONFIGWIDGETS_VERSION < QT_VERSION_CHECK(5,32,0) + * if (KCoreAddons::version() < QT_VERSION_CHECK(5,32,0)) { + * KConfigDialogManager::changedMap()->insert("OtherColorEditWidget", SIGNAL(colorSelected(QColor))); + * } + * #endif * \endcode - * In this case you won't need to add the widget's class name to propertyMap(). - * Alternatively you can set the kcfg_property using designer. * * @author Benjamin C Meyer * @author Waldo Bastian @@ -162,6 +258,10 @@ /** * Retrieve the map between widgets class names and signals that are listened * to detect changes in the configuration values. + * @deprecated For code having KDE Frameworks 5.32 as minimal required version, + * rely on the change signal noted with NOTIFY in the definition of the + * used property instead of setting it in this map. Or set the + * "kcfg_propertyNotify" property on the widget instance. */ static QHash *changedMap(); @@ -220,14 +320,28 @@ QByteArray getUserProperty(const QWidget *widget) const; /** - * Find the property to use for a widget by querying the kcfg_property + * Find the property to use for a widget by querying the "kcfg_property" * property of the widget. Like a widget can use a property other than the * USER property. * @since 4.3 */ QByteArray getCustomProperty(const QWidget *widget) const; /** + * Finds the changed signal of the USER property using Qt's MetaProperty system. + * @since 5.32 + */ + QByteArray getUserPropertyChangedSignal(const QWidget *widget) const; + + /** + * Find the changed signal of the property to use for a widget by querying + * the "kcfg_propertyNotify" property of the widget. Like a widget can use a + * property change signal other than the one for USER property, if there even is one. + * @since 5.32 + */ + QByteArray getCustomPropertyChangedSignal(const QWidget *widget) const; + + /** * Set a property */ void setProperty(QWidget *w, const QVariant &v); diff --git a/src/kconfigdialogmanager.cpp b/src/kconfigdialogmanager.cpp --- a/src/kconfigdialogmanager.cpp +++ b/src/kconfigdialogmanager.cpp @@ -84,6 +84,7 @@ delete d; } +// 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()) { @@ -233,6 +234,9 @@ return valueChanged; } + const QMetaMethod widgetModifiedSignal = metaObject()->method(metaObject()->indexOfSignal("widgetModified()")); + Q_ASSERT(widgetModifiedSignal.isValid() && metaObject()->indexOfSignal("widgetModified()")>=0); + foreach (QObject *object, listOfChildren) { if (!object->isWidgetType()) { continue; // Skip non-widgets @@ -254,34 +258,47 @@ setupWidget(childWidget, item); if (trackChanges) { + bool changeSignalFound = false; if (d->allExclusiveGroupBoxes.contains(childWidget)) { const QList buttons = childWidget->findChildren(); foreach(QAbstractButton *button, buttons) { connect(button, SIGNAL(toggled(bool)), this, SIGNAL(widgetModified())); } } - QHash::const_iterator changedIt = s_changedMap()->constFind(childWidget->metaObject()->className()); + QByteArray propertyChangeSignal = getCustomPropertyChangedSignal(childWidget); + if (propertyChangeSignal.isEmpty()) { + propertyChangeSignal = getUserPropertyChangedSignal(childWidget); + } - 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 (childWidget->metaObject()->superClass()) { - changedIt = s_changedMap()->constFind(childWidget->metaObject()->superClass()->className()); + 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); + if (indexOfProperty != -1) { + const QMetaProperty property = metaObject->property(indexOfProperty); + const QMetaMethod notifySignal = property.notifySignal(); + if (notifySignal.isValid()) { + connect(childWidget, notifySignal, this, widgetModifiedSignal); + changeSignalFound = true; + } + } } else { - changedIt = s_changedMap()->constFind(nullptr); + qWarning() << "Don't know how to monitor widget '" << childWidget->metaObject()->className() << "' for changes!"; } - } - - if (changedIt == s_changedMap()->constEnd()) { - qWarning() << "Don't know how to monitor widget '" << childWidget->metaObject()->className() << "' for changes!"; } else { - connect(childWidget, *changedIt, + connect(childWidget, propertyChangeSignal, this, SIGNAL(widgetModified())); + changeSignalFound = true; + } + if (changeSignalFound) { QComboBox *cb = qobject_cast(childWidget); if (cb && cb->isEditable()) connect(cb, SIGNAL(editTextChanged(QString)), @@ -426,6 +443,7 @@ const char *widgetUserPropertyName = widget->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 } @@ -448,6 +466,37 @@ return QByteArray(); } +QByteArray KConfigDialogManager::getUserPropertyChangedSignal(const QWidget *widget) const +{ + QHash::const_iterator changedIt = s_changedMap()->constFind(widget->metaObject()->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()) { + changedIt = s_changedMap()->constFind(widget->metaObject()->superClass()->className()); + } + } + + 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(QVariant::ByteArray)) { + qWarning() << "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)) {