diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1905d62..707bc72 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,8 @@ set(SOURCES iconproducer.cpp powerbutton.cpp ../config/powermanagementsettings.cpp + PowerProfiles.cpp + DBusPropAsyncGetter.cpp ) set(UI_FILES diff --git a/src/DBusPropAsyncGetter.cpp b/src/DBusPropAsyncGetter.cpp new file mode 100644 index 0000000..816b51b --- /dev/null +++ b/src/DBusPropAsyncGetter.cpp @@ -0,0 +1,103 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2025~ LXQt team + * Authors: + * Palo Kisa + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include "DBusPropAsyncGetter.h" +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; + +namespace LXQt +{ + DBusPropAsyncGetter::DBusPropAsyncGetter(const QString & service, const QString & path, const QString & interface, const QDBusConnection & conn) + : mService{service} + , mPath{path} + , mInterface{interface} + , mConn{conn} + { + if (!mConn.connect(mService + , mPath + , "org.freedesktop.DBus.Properties"_L1 + , "PropertiesChanged"_L1 + , this + , SLOT(onPropertiesChanged(QString, QVariantMap, QStringList)) + ) + ) + qDebug().noquote().nospace() << "Could not connect to org.freedesktop.DBus.Properties.PropertiesChanged()(" << mService << ',' << mPath << ')'; + + connect(new QDBusServiceWatcher{mService, mConn, QDBusServiceWatcher::WatchForUnregistration, this} + , &QDBusServiceWatcher::serviceUnregistered + , this + , [this] { Q_EMIT serviceDisappeared(); } + ); + } + + void DBusPropAsyncGetter::fetch(const QString name) + { + QDBusMessage msg = QDBusMessage::createMethodCall(mService, mPath, "org.freedesktop.DBus.Properties"_L1, "Get"_L1); + msg << mInterface << name; + connect(new QDBusPendingCallWatcher{mConn.asyncCall(msg), this} + , &QDBusPendingCallWatcher::finished + , this + , [this, name] (QDBusPendingCallWatcher * call) { + QDBusPendingReply reply = *call; + if (reply.isError()) + qDebug().noquote().nospace() << "Error on DBus request(" << mService << ',' << mPath << ',' << name << "): " << reply.error(); + Q_EMIT fetched(name, reply.value()); + call->deleteLater(); + } + ); + } + + void DBusPropAsyncGetter::push(const QString name, const QVariant value) + { + QDBusMessage msg = QDBusMessage::createMethodCall(mService, mPath, "org.freedesktop.DBus.Properties"_L1, "Set"_L1); + msg << mInterface << name << value; + connect(new QDBusPendingCallWatcher{mConn.asyncCall(msg), this} + , &QDBusPendingCallWatcher::finished + , this + , [this, name] (QDBusPendingCallWatcher * call) { + QDBusPendingReply<> reply = *call; + if (reply.isError()) + qDebug().noquote().nospace() << "Error on DBus request(" << mService << ',' << mPath << ',' << mInterface << ',' << name << "): " << reply.error(); + else + Q_EMIT pushed(name); + call->deleteLater(); + } + ); + } + + void DBusPropAsyncGetter::onPropertiesChanged(const QString & /*interfaceName*/, const QVariantMap & changedProperties, const QStringList & invalidatedProperties) + { + for (const auto & [key, value] : changedProperties.asKeyValueRange()) + Q_EMIT fetched(key, value); + for (const auto & key : invalidatedProperties) + Q_EMIT fetched(key, {}); + } +} diff --git a/src/DBusPropAsyncGetter.h b/src/DBusPropAsyncGetter.h new file mode 100644 index 0000000..9a40e3b --- /dev/null +++ b/src/DBusPropAsyncGetter.h @@ -0,0 +1,61 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2025~ LXQt team + * Authors: + * Palo Kisa + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#pragma once + +#include +#include + +namespace LXQt +{ + class DBusPropAsyncGetter : public QObject + { + Q_OBJECT + public: + DBusPropAsyncGetter(const QString & service, const QString & path, const QString & interface, const QDBusConnection & conn); + + public Q_SLOTS: + // Note: interthread signal/slot communication - parameters by values + void fetch(const QString name); + void push(const QString name, const QVariant value); + + public Q_SLOTS: + void onPropertiesChanged(const QString & interfaceName, const QVariantMap & changedProperties, const QStringList & invalidatedProperties); + + Q_SIGNALS: + // Note: interthread signal/slot communication - parameters by values + void fetched(const QString name, const QVariant value); + void pushed(const QString name); + void serviceDisappeared(); + + private: + const QString mService; + const QString mPath; + const QString mInterface; + QDBusConnection mConn; + }; +} diff --git a/src/PowerProfiles.cpp b/src/PowerProfiles.cpp new file mode 100644 index 0000000..99e87fe --- /dev/null +++ b/src/PowerProfiles.cpp @@ -0,0 +1,164 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2025~ LXQt team + * Authors: + * Palo Kisa + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include +#include +#include +#include +#include +#include "PowerProfiles.h" +#include "DBusPropAsyncGetter.h" + +namespace { + using Trans = struct { char const * const s1; char const * const s2; }; + [[maybe_unused]] void just_for_translations_dummy_function(const Trans = QT_TRANSLATE_NOOP3("LXQt::PowerProfiles", "power-saver", "power-profiles-daemon") + , const Trans = QT_TRANSLATE_NOOP3("LXQt::PowerProfiles", "balanced", "power-profiles-daemon") + , const Trans = QT_TRANSLATE_NOOP3("LXQt::PowerProfiles", "performance", "power-profiles-daemon") + ) + {} +} + +namespace LXQt +{ + Q_GLOBAL_STATIC(LXQt::PowerProfiles, g_power_profiles) + + const QString PowerProfiles::msDBusPPService = QStringLiteral("org.freedesktop.UPower.PowerProfiles"); + const QString PowerProfiles::msDBusPPPath = QStringLiteral("/org/freedesktop/UPower/PowerProfiles"); + const QString PowerProfiles::msDBusPPInterface = msDBusPPService; + const QString PowerProfiles::msDBusPPProperties[] = { QStringLiteral("ActiveProfile"), QStringLiteral("Profiles") }; + + PowerProfiles & PowerProfiles::instance() + { + return *g_power_profiles; + } + + PowerProfiles::PowerProfiles(QObject * parent/* = nullptr*/) + : QObject{parent} + , mThread{new QThread} + , mPropGetter{new DBusPropAsyncGetter{msDBusPPService, msDBusPPPath, msDBusPPInterface, QDBusConnection::systemBus()}} + , mMenuAction{new QAction} + , mMenu{new QMenu} + + { + mMenuAction->setMenu(mMenu.get()); + mMenu->menuAction()->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-performance"))); + mMenu->setTitle(tr("Power profile")); + + // register the DBus types + qRegisterMetaType("QListOfQVariantMap"); + qDBusRegisterMetaType(); + + // we want to dispatch DBus calls on dedicated thread + mPropGetter->moveToThread(mThread.get()); + + // signals forwarding + connect(mPropGetter.get(), &DBusPropAsyncGetter::fetched, this, &PowerProfiles::propertyFetched); + connect(mPropGetter.get(), &DBusPropAsyncGetter::pushed, this, &PowerProfiles::propertyPushed); + + // interested signals for our processing + connect(mPropGetter.get(), &DBusPropAsyncGetter::fetched, this, &PowerProfiles::onFetched); + connect(mPropGetter.get(), &DBusPropAsyncGetter::serviceDisappeared, this, [this] { reassembleMenu({}); }); + + // our generated signals for dispatching into dedicated thread + connect(this, &PowerProfiles::needPropertyFetch, mPropGetter.get(), &DBusPropAsyncGetter::fetch); + connect(this, &PowerProfiles::needPropertyPush, mPropGetter.get(), &DBusPropAsyncGetter::push); + + mThread->start(); + + reassembleMenu({}); + + Q_EMIT needPropertyFetch(msDBusPPProperties[PPP_ActiveProfile]); + Q_EMIT needPropertyFetch(msDBusPPProperties[PPP_Profiles]); + } + + PowerProfiles::~PowerProfiles() + { + mThread->quit(); + mThread->wait(); + } + + QAction * PowerProfiles::menuAction() + { + return mMenuAction.get(); + } + + void PowerProfiles::onFetched(const QString name, const QVariant value) + { + if (name == msDBusPPProperties[PPP_ActiveProfile]) { + const QString profile = qdbus_cast(value); + if (mActiveProfile == profile) + return; + mActiveProfile = profile; + if (mActions) + for (auto const & action : mActions->actions()) + action->setChecked(get(action->data()) == mActiveProfile); + } else if (name == msDBusPPProperties[PPP_Profiles]) { + reassembleMenu(qdbus_cast(value)); + } + } + + void PowerProfiles::reassembleMenu(const QListOfQVariantMap & profiles) + { + mMenu->clear(); + if (profiles.empty()) + { + mActions.reset(nullptr); + mMenuAction->setVisible(false); + return; + } + + mMenuAction->setVisible(true); + mActions.reset(new QActionGroup(nullptr)); + mActions->setExclusionPolicy(QActionGroup::ExclusionPolicy::Exclusive); + + connect(mActions.get(), &QActionGroup::triggered, this, [this] (const QAction * a) { + setActiveProfile(get(a->data())); + }); + + for (auto const & profile : profiles) + { + const QString p = qdbus_cast(profile[QStringLiteral("Profile")]); + auto a = new QAction(tr(qUtf8Printable(p), "power-profiles-daemon"), mActions.get()); + a->setCheckable(true); + a->setData(p); + a->setChecked(mActiveProfile == p); + } + mMenu->addActions(mActions->actions()); + } + + const QString & PowerProfiles::activeProfile() const + { + return mActiveProfile; + } + + void PowerProfiles::setActiveProfile(const QString &value) + { + // Note: Just emit the signal to make change. Don't modify any internal state. + // We will be notified by the fetched() signal upon sucessfull PropertyChanged. + Q_EMIT needPropertyPush(msDBusPPProperties[PPP_ActiveProfile], QVariant::fromValue(QDBusVariant{value})); + } +} diff --git a/src/PowerProfiles.h b/src/PowerProfiles.h new file mode 100644 index 0000000..ad56479 --- /dev/null +++ b/src/PowerProfiles.h @@ -0,0 +1,92 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2025~ LXQt team + * Authors: + * Palo Kisa + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#pragma once + +#include +#include "dbus_types.h" + +class QAction; +class QMenu; +class QActionGroup; + +namespace LXQt +{ + class DBusPropAsyncGetter; + + class PowerProfiles : public QObject + { + Q_OBJECT + public: + enum PPProperties + { + PPP_ActiveProfile = 0 + , PPP_Profiles + , PPP_Count + }; + public: + static PowerProfiles & instance(); + + public: + PowerProfiles(QObject * parent = nullptr); + ~PowerProfiles(); + + bool isValid() const; + const QString & activeProfile() const; + void setActiveProfile(const QString & value); + QAction * menuAction(); + + Q_SIGNALS: + void needPropertyFetch(const QString & name); + void needPropertyPush(const QString & name, const QVariant & value); + void propertyFetched(const QString & name, const QVariant & value); + void propertyPushed(const QString & name); + + protected: + void onFetched(const QString name, const QVariant value); + void reassembleMenu(const QListOfQVariantMap & profiles); + + private: + static const QString msDBusPPService; + static const QString msDBusPPPath; + static const QString msDBusPPInterface; + static const QString msDBusPPProperties[PPP_Count]; + + private: + // Note: we don't want to rely on the qdbusxml2cpp generated interface as it + // generates fetching/setting properties as DBus synch calls and we must not be blocking + // on the main/ui thread. + //std::unique_ptr mDBusProfiles; + std::unique_ptr mThread; + std::unique_ptr mPropGetter; + + std::unique_ptr mMenuAction; + std::unique_ptr mMenu; + std::unique_ptr mActions; + QString mActiveProfile; + }; +} diff --git a/src/dbus_types.h b/src/dbus_types.h new file mode 100644 index 0000000..f1f089a --- /dev/null +++ b/src/dbus_types.h @@ -0,0 +1,8 @@ +# pragma once + +#include +#include + +using QListOfQVariantMap = QList; + + diff --git a/src/org.freedesktop.UPower.PowerProfiles.xml b/src/org.freedesktop.UPower.PowerProfiles.xml new file mode 100644 index 0000000..f2e18da --- /dev/null +++ b/src/org.freedesktop.UPower.PowerProfiles.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/trayicon.cpp b/src/trayicon.cpp index eec8532..d5a03c6 100644 --- a/src/trayicon.cpp +++ b/src/trayicon.cpp @@ -37,8 +37,9 @@ #include #include #include -#include +#include #include +#include #include "trayicon.h" #include "batteryhelper.h" @@ -46,6 +47,7 @@ #include #include +#include "PowerProfiles.h" TrayIcon::TrayIcon(Solid::Battery *battery, QObject *parent) : QSystemTrayIcon(parent), @@ -65,7 +67,7 @@ TrayIcon::TrayIcon(Solid::Battery *battery, QObject *parent) connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated); - mContextMenu.addAction(XdgIcon::fromTheme(QStringLiteral("configure")), tr("Configure"), + mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("configure")), tr("Configure"), this, &TrayIcon::onConfigureTriggered); // pause actions @@ -93,15 +95,18 @@ TrayIcon::TrayIcon(Solid::Battery *battery, QObject *parent) a->setCheckable(true); a->setData(PAUSE::Four); - QMenu *pauseMenu = mContextMenu.addMenu(XdgIcon::fromTheme(QStringLiteral("media-playback-pause")), + QMenu *pauseMenu = mContextMenu.addMenu(QIcon::fromTheme(QStringLiteral("media-playback-pause")), tr("Pause idleness checks")); pauseMenu->addActions(mPauseActions->actions()); + // power-profiles actions + mContextMenu.addAction(LXQt::PowerProfiles::instance().menuAction()); + mContextMenu.addSeparator(); - mContextMenu.addAction(XdgIcon::fromTheme(QStringLiteral("help-about")), tr("About"), + mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("help-about")), tr("About"), this, &TrayIcon::onAboutTriggered); - mContextMenu.addAction(XdgIcon::fromTheme(QStringLiteral("edit-delete")), tr("Disable icon"), + mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), tr("Disable icon"), this, &TrayIcon::onDisableIconTriggered); setContextMenu(&mContextMenu); }