uisupport: Provide helpers for dealing with widget changes
authorManuel Nickschas <sputnick@quassel-irc.org>
Mon, 17 Sep 2018 21:42:37 +0000 (23:42 +0200)
committerManuel Nickschas <sputnick@quassel-irc.org>
Sun, 18 Nov 2018 10:06:43 +0000 (11:06 +0100)
In many places, we want to connect to a widget's changed signal,
i.e. a signal that is emitted whenever the widget changes. This
requires a great deal of boilerplate especially in settingspages,
since the signal to listen to varies between widget types.

Provide helpers that automate most of this by matching the given
widget against a static list of supported widgets' changed signals,
and connecting automatically. A sprinkle of template magic makes
it very easy to support a bunch of different widget types in a generic
way.

Make use of this feature to get rid of the remaining old-style
connects in SettingsPage. Also migrate a couple of specific
settingspages as proof-of-concept and API validation; others
can follow over time.

src/qtui/settingspages/backlogsettingspage.cpp
src/qtui/settingspages/networkssettingspage.cpp
src/qtui/settingspages/networkssettingspage.h
src/uisupport/CMakeLists.txt
src/uisupport/settingspage.cpp
src/uisupport/widgethelpers.h [new file with mode: 0644]

index f0ec4ab..541fb55 100644 (file)
 
 #include "backlogsettingspage.h"
 
 
 #include "backlogsettingspage.h"
 
-#include "qtui.h"
 #include "backlogsettings.h"
 #include "backlogsettings.h"
-
-// For backlog requester types
 #include "backlogrequester.h"
 #include "backlogrequester.h"
+#include "qtui.h"
+#include "widgethelpers.h"
 
 BacklogSettingsPage::BacklogSettingsPage(QWidget *parent)
     : SettingsPage(tr("Interface"), tr("Backlog Fetching"), parent)
 
 BacklogSettingsPage::BacklogSettingsPage(QWidget *parent)
     : SettingsPage(tr("Interface"), tr("Backlog Fetching"), parent)
@@ -36,7 +35,7 @@ BacklogSettingsPage::BacklogSettingsPage(QWidget *parent)
     // FIXME: global backlog requester disabled until issues ruled out
     ui.requesterType->removeItem(2);
 
     // FIXME: global backlog requester disabled until issues ruled out
     ui.requesterType->removeItem(2);
 
-    connect(ui.requesterType, selectOverload<int>(&QComboBox::currentIndexChanged), this, &BacklogSettingsPage::widgetHasChanged);
+    connectToWidgetChangedSignal(ui.requesterType, this, &BacklogSettingsPage::widgetHasChanged);
 }
 
 
 }
 
 
index 050ffc5..a886d92 100644 (file)
@@ -32,6 +32,7 @@
 #include "presetnetworks.h"
 #include "settingspagedlg.h"
 #include "util.h"
 #include "presetnetworks.h"
 #include "settingspagedlg.h"
 #include "util.h"
+#include "widgethelpers.h"
 
 // IRCv3 capabilities
 #include "irccap.h"
 
 // IRCv3 capabilities
 #include "irccap.h"
@@ -63,8 +64,6 @@ NetworksSettingsPage::NetworksSettingsPage(QWidget *parent)
     ui.downServer->setIcon(icon::get("go-down"));
     ui.editIdentities->setIcon(icon::get("configure"));
 
     ui.downServer->setIcon(icon::get("go-down"));
     ui.editIdentities->setIcon(icon::get("configure"));
 
-    _ignoreWidgetChanges = false;
-
     connectedIcon = icon::get("network-connect");
     connectingIcon = icon::get("network-wired"); // FIXME network-connecting
     disconnectedIcon = icon::get("network-disconnect");
     connectedIcon = icon::get("network-connect");
     connectingIcon = icon::get("network-wired"); // FIXME network-connecting
     disconnectedIcon = icon::get("network-disconnect");
@@ -85,38 +84,37 @@ NetworksSettingsPage::NetworksSettingsPage(QWidget *parent)
     currentId = 0;
     setEnabled(Client::isConnected()); // need a core connection!
     setWidgetStates();
     currentId = 0;
     setEnabled(Client::isConnected()); // need a core connection!
     setWidgetStates();
+
+    connectToWidgetsChangedSignals({
+            ui.identityList,
+            ui.performEdit,
+            ui.sasl,
+            ui.saslAccount,
+            ui.saslPassword,
+            ui.autoIdentify,
+            ui.autoIdentifyService,
+            ui.autoIdentifyPassword,
+            ui.useCustomEncodings,
+            ui.sendEncoding,
+            ui.recvEncoding,
+            ui.serverEncoding,
+            ui.autoReconnect,
+            ui.reconnectInterval,
+            ui.reconnectRetries,
+            ui.unlimitedRetries,
+            ui.rejoinOnReconnect,
+            ui.useCustomMessageRate,
+            ui.messageRateBurstSize,
+            ui.messageRateDelay,
+            ui.unlimitedMessageRate
+    }, this, &NetworksSettingsPage::widgetHasChanged);
+
     connect(Client::instance(), &Client::coreConnectionStateChanged, this, &NetworksSettingsPage::coreConnectionStateChanged);
     connect(Client::instance(), &Client::networkCreated, this, &NetworksSettingsPage::clientNetworkAdded);
     connect(Client::instance(), &Client::networkRemoved, this, &NetworksSettingsPage::clientNetworkRemoved);
     connect(Client::instance(), &Client::identityCreated, this, &NetworksSettingsPage::clientIdentityAdded);
     connect(Client::instance(), &Client::identityRemoved, this, &NetworksSettingsPage::clientIdentityRemoved);
 
     connect(Client::instance(), &Client::coreConnectionStateChanged, this, &NetworksSettingsPage::coreConnectionStateChanged);
     connect(Client::instance(), &Client::networkCreated, this, &NetworksSettingsPage::clientNetworkAdded);
     connect(Client::instance(), &Client::networkRemoved, this, &NetworksSettingsPage::clientNetworkRemoved);
     connect(Client::instance(), &Client::identityCreated, this, &NetworksSettingsPage::clientIdentityAdded);
     connect(Client::instance(), &Client::identityRemoved, this, &NetworksSettingsPage::clientIdentityRemoved);
 
-    connect(ui.identityList, selectOverload<int>(&QComboBox::currentIndexChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    //connect(ui.randomServer, &QAbstractButton::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.performEdit, &QTextEdit::textChanged, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.sasl, &QGroupBox::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.saslAccount, &QLineEdit::textEdited, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.saslPassword, &QLineEdit::textEdited, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.autoIdentify, &QGroupBox::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.autoIdentifyService, &QLineEdit::textEdited, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.autoIdentifyPassword, &QLineEdit::textEdited, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.useCustomEncodings, &QGroupBox::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.sendEncoding, selectOverload<int>(&QComboBox::currentIndexChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.recvEncoding, selectOverload<int>(&QComboBox::currentIndexChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.serverEncoding, selectOverload<int>(&QComboBox::currentIndexChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.autoReconnect, &QGroupBox::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.reconnectInterval, selectOverload<int>(&QSpinBox::valueChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.reconnectRetries, selectOverload<int>(&QSpinBox::valueChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.unlimitedRetries, &QAbstractButton::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.rejoinOnReconnect, &QAbstractButton::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-
-    // Core features can change during a reconnect.  Always connect these here, delaying testing for
-    // the core feature flag in load().
-    connect(ui.useCustomMessageRate, &QGroupBox::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.messageRateBurstSize, selectOverload<int>(&QSpinBox::valueChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.messageRateDelay, selectOverload<double>(&QDoubleSpinBox::valueChanged), this, &NetworksSettingsPage::widgetHasChanged);
-    connect(ui.unlimitedMessageRate, &QAbstractButton::clicked, this, &NetworksSettingsPage::widgetHasChanged);
-
     foreach(IdentityId id, Client::identityIds()) {
         clientIdentityAdded(id);
     }
     foreach(IdentityId id, Client::identityIds()) {
         clientIdentityAdded(id);
     }
index 965540e..74b21d6 100644 (file)
@@ -132,7 +132,7 @@ private:
 
     NetworkId currentId;
     QHash<NetworkId, NetworkInfo> networkInfos;
 
     NetworkId currentId;
     QHash<NetworkId, NetworkInfo> networkInfos;
-    bool _ignoreWidgetChanges;
+    bool _ignoreWidgetChanges{false};
 #ifdef HAVE_SSL
     CertIdentity *_cid{nullptr};
 #endif
 #ifdef HAVE_SSL
     CertIdentity *_cid{nullptr};
 #endif
index 839ecc8..3b19471 100644 (file)
@@ -32,6 +32,7 @@ target_sources(${TARGET} PRIVATE
     treeviewtouch.cpp
     uisettings.cpp
     uistyle.cpp
     treeviewtouch.cpp
     uisettings.cpp
     uistyle.cpp
+    widgethelpers.h
 
     # needed for automoc
     abstractnotificationbackend.h
 
     # needed for automoc
     abstractnotificationbackend.h
index ed48f07..152428d 100644 (file)
@@ -27,8 +27,8 @@
 #include <utility>
 
 #include "fontselector.h"
 #include <utility>
 
 #include "fontselector.h"
-
 #include "uisettings.h"
 #include "uisettings.h"
+#include "widgethelpers.h"
 
 SettingsPage::SettingsPage(QString category, QString title, QWidget *parent)
     : QWidget(parent),
 
 SettingsPage::SettingsPage(QString category, QString title, QWidget *parent)
     : QWidget(parent),
@@ -113,21 +113,8 @@ void SettingsPage::initAutoWidgets()
     // we need to climb the QObject tree recursively
     findAutoWidgets(this, &_autoWidgets);
 
     // we need to climb the QObject tree recursively
     findAutoWidgets(this, &_autoWidgets);
 
-    foreach(QObject *widget, _autoWidgets) {
-        if (widget->inherits("ColorButton"))
-            connect(widget, SIGNAL(colorChanged(QColor)), SLOT(autoWidgetHasChanged()));
-        else if (widget->inherits("QAbstractButton") || widget->inherits("QGroupBox"))
-            connect(widget, SIGNAL(toggled(bool)), SLOT(autoWidgetHasChanged()));
-        else if (widget->inherits("QLineEdit") || widget->inherits("QTextEdit"))
-            connect(widget, SIGNAL(textChanged(const QString &)), SLOT(autoWidgetHasChanged()));
-        else if (widget->inherits("QComboBox"))
-            connect(widget, SIGNAL(currentIndexChanged(int)), SLOT(autoWidgetHasChanged()));
-        else if (widget->inherits("QSpinBox"))
-            connect(widget, SIGNAL(valueChanged(int)), SLOT(autoWidgetHasChanged()));
-        else if (widget->inherits("FontSelector"))
-            connect(widget, SIGNAL(fontChanged(QFont)), SLOT(autoWidgetHasChanged()));
-        else
-            qWarning() << "SettingsPage::init(): Unknown autoWidget type" << widget->metaObject()->className();
+    if (!connectToWidgetsChangedSignals(_autoWidgets, this, &SettingsPage::autoWidgetHasChanged)) {
+        qWarning() << "SettingsPage::initAutoWidgets(): Unsupported auto widget type(s)!";
     }
 }
 
     }
 }
 
diff --git a/src/uisupport/widgethelpers.h b/src/uisupport/widgethelpers.h
new file mode 100644 (file)
index 0000000..22f9f9c
--- /dev/null
@@ -0,0 +1,136 @@
+/***************************************************************************
+ *   Copyright (C) 2005-2018 by the Quassel Project                        *
+ *   devel@quassel-irc.org                                                 *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) version 3.                                           *
+ *                                                                         *
+ *   This program 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 General Public License for more details.                          *
+ *                                                                         *
+ *   You should have received a copy of the GNU General Public License     *
+ *   along with this program; if not, write to the                         *
+ *   Free Software Foundation, Inc.,                                       *
+ *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
+ ***************************************************************************/
+
+#pragma once
+
+#include <algorithm>
+#include <initializer_list>
+#include <tuple>
+#include <utility>
+
+#include <QAbstractButton>
+#include <QComboBox>
+#include <QDoubleSpinBox>
+#include <QGroupBox>
+#include <QLineEdit>
+#include <QObject>
+#include <QSpinBox>
+#include <QTextEdit>
+
+#include "colorbutton.h"
+#include "fontselector.h"
+#include "funchelpers.h"
+#include "util.h"
+
+namespace detail {
+
+/**
+ * Contains all supported widget changed signals.
+ */
+static const auto supportedWidgetChangedSignals = std::make_tuple(
+            &ColorButton::colorChanged,
+            &FontSelector::fontChanged,
+            &QAbstractButton::toggled,
+            selectOverload<int>(&QComboBox::currentIndexChanged),
+            selectOverload<double>(&QDoubleSpinBox::valueChanged),
+            &QGroupBox::toggled,
+            &QLineEdit::textChanged,
+            selectOverload<int>(&QSpinBox::valueChanged),
+            &QTextEdit::textChanged
+);
+
+/**
+ * Tries to find a changed signal that matches the given widget type, and connects that to the given receiver/slot.
+ */
+template<typename Receiver, typename Slot, std::size_t ...Is>
+bool tryConnectChangedSignal(const QObject* widget, const Receiver* receiver, Slot slot, std::index_sequence<Is...>)
+{
+    // Tries to cast the given QObject to the given signal's class type, and connects to receiver/slot if successful.
+    // If *alreadyConnected is true, just returns false to prevent multiple connections.
+    static const auto tryConnect = [](const QObject* object, auto sig, auto receiver, auto slot, bool* alreadyConnected) {
+        if (!*alreadyConnected) {
+            auto widget = qobject_cast<const typename MemberFunction<decltype(sig)>::ClassType *>(object);
+            if (widget) {
+                *alreadyConnected = QObject::connect(widget, sig, receiver, slot);
+                return *alreadyConnected;
+            }
+        }
+        return false;
+    };
+
+    // Unpack the tuple and try to connect to each contained signal in order
+    bool alreadyConnected{false};
+    auto results = { tryConnect(widget, std::get<Is>(supportedWidgetChangedSignals), receiver, slot, &alreadyConnected)... };
+    return std::any_of(results.begin(), results.end(), [](bool result) { return result; });
+}
+
+}  //detail
+
+/**
+ * Connects the given widget's changed signal to the given receiver/context and slot.
+ *
+ * Changed signals for supported widgets are listed in detail::supportedWidgetChangedSignals.
+ *
+ * @note The slot must not take any arguments.
+ *
+ * @param widget   Pointer to the widget
+ * @param receiver Receiver of the signal (or context for the connection)
+ * @param slot     Receiving slot (or functor, or whatever)
+ * @returns true if the widget type is supported, false otherwise
+ */
+template<typename Receiver, typename Slot>
+bool connectToWidgetChangedSignal(const QObject* widget, const Receiver* receiver, Slot slot)
+{
+    return detail::tryConnectChangedSignal(widget, receiver, slot,
+                                           std::make_index_sequence<std::tuple_size<decltype(detail::supportedWidgetChangedSignals)>::value>{});
+}
+
+/**
+ * Connects the given widgets' changed signals to the given receiver/context and slot.
+ *
+ * Changed signals for supported widgets are listed in detail::supportedWidgetChangedSignals.
+ *
+ * @note The slot must not take any arguments.
+ *
+ * @param widget   Pointer to the widget
+ * @param receiver Receiver of the signal (or context for the connection)
+ * @param slot     Receiving slot (or functor, or whatever)
+ * @returns true if all given widget types are supported, false otherwise
+ */
+template<typename Container, typename Receiver, typename Slot>
+bool connectToWidgetsChangedSignals(const Container& widgets, const Receiver* receiver, Slot slot)
+{
+    bool success = true;
+    for (auto&& widget : widgets) {
+        success &= detail::tryConnectChangedSignal(widget, receiver, slot,
+                                                   std::make_index_sequence<std::tuple_size<decltype(detail::supportedWidgetChangedSignals)>::value>{});
+    }
+    return success;
+}
+
+/**
+ * @overload
+ * Convenience overload that allows brace-initializing the list of widgets.
+ */
+template<typename Receiver, typename Slot>
+bool connectToWidgetsChangedSignals(const std::initializer_list<const QObject*>& widgets, const Receiver* receiver, Slot slot)
+{
+    return connectToWidgetsChangedSignals(std::vector<const QObject*>{widgets}, receiver, slot);
+}