Skip to content

Power profiles #426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ set(SOURCES
iconproducer.cpp
powerbutton.cpp
../config/powermanagementsettings.cpp
PowerProfiles.cpp
DBusPropAsyncGetter.cpp
)

set(UI_FILES
Expand Down
103 changes: 103 additions & 0 deletions src/DBusPropAsyncGetter.cpp
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*
* 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 <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDBusServiceWatcher>

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<QVariant> 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, {});
}
}
61 changes: 61 additions & 0 deletions src/DBusPropAsyncGetter.h
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*
* 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 <QObject>
#include <QDBusConnection>

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;
};
}
164 changes: 164 additions & 0 deletions src/PowerProfiles.cpp
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*
* 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 <QMenu>
#include <QActionGroup>
#include <QAction>
#include <QThread>
#include <QDBusMetaType>
#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>("QListOfQVariantMap");
qDBusRegisterMetaType<QListOfQVariantMap>();

// 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<QString>(value);
if (mActiveProfile == profile)
return;
mActiveProfile = profile;
if (mActions)
for (auto const & action : mActions->actions())
action->setChecked(get<QString>(action->data()) == mActiveProfile);
} else if (name == msDBusPPProperties[PPP_Profiles]) {
reassembleMenu(qdbus_cast<QListOfQVariantMap>(value));
}
}

void PowerProfiles::reassembleMenu(const QListOfQVariantMap & profiles)
{
mMenu->clear();
if (profiles.empty())
{
mActions.reset(nullptr);
mMenuAction->setVisible(false);
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little uncomfortable about showing the new menu-item when power-profiles-daemon isn't available. This may pretend that power-profiles-daemon is preferred by LXQt, while many users may install tlp, which is in conflict with it.

IMHO, this deserves a discussion before merging.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we don't necessarily depend on power-profiles-daemon, but on provided DBus service. Should we rather use org.freedesktop.UPower.PowerProfiles DBus service not available?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

power-profiles-daemon is an actual package that the user can install to fix the problem, so it is much more user friendly imo.

Copy link
Member

@tsujan tsujan Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rather use …

Too technical.

Why not hiding the menu-item instead?

EDIT: I mean the Power Profile menu-item, not just its submenu.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not hiding the menu-item instead?

OK. Done


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<QString>(a->data()));
});

for (auto const & profile : profiles)
{
const QString p = qdbus_cast<QString>(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}));
}
}
Loading