mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 15:03:09 +00:00
Add gaming session control backends
Add PowerProfileControl and GameModeControl singletons and wire them into gaming mode lifecycle handling. When gaming mode turns on, keep DND on, switch to the performance profile when available, and request GameMode. Restore previous state when gaming mode turns off. Add an overlayEnabled property in GameLauncherProvider so the launcher can toggle MangoHud environment variables from QML.
This commit is contained in:
parent
bcb985c652
commit
62243b7f64
8 changed files with 353 additions and 5 deletions
|
|
@ -2,7 +2,7 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
find_package(SDL3 REQUIRED)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Sql)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Sql DBus)
|
||||
|
||||
ecm_add_qml_module(gamingshellplugin URI org.kde.plasma.private.mobileshell.gamingshellplugin GENERATE_PLUGIN_SOURCE)
|
||||
|
||||
|
|
@ -10,10 +10,13 @@ target_sources(gamingshellplugin PRIVATE
|
|||
gamepadmanager.cpp
|
||||
gamepaddevice.cpp
|
||||
gamelauncherprovider.cpp
|
||||
powerprofilecontrol.cpp
|
||||
gamemodecontrol.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(gamingshellplugin PRIVATE
|
||||
Qt::Core
|
||||
Qt::DBus
|
||||
Qt::Qml
|
||||
Qt::Quick
|
||||
Qt::Sql
|
||||
|
|
|
|||
|
|
@ -533,6 +533,29 @@ void GameLauncherProvider::setSourceFilter(const QString &source)
|
|||
applyFilter();
|
||||
}
|
||||
|
||||
bool GameLauncherProvider::overlayEnabled() const
|
||||
{
|
||||
return m_overlayEnabled;
|
||||
}
|
||||
|
||||
void GameLauncherProvider::setOverlayEnabled(bool enabled)
|
||||
{
|
||||
if (m_overlayEnabled == enabled) {
|
||||
return;
|
||||
}
|
||||
m_overlayEnabled = enabled;
|
||||
Q_EMIT overlayEnabledChanged();
|
||||
|
||||
// Set/unset MangoHud environment variables for child processes
|
||||
if (enabled) {
|
||||
qputenv("MANGOHUD", "1");
|
||||
qputenv("MANGOHUD_DLSYM", "1");
|
||||
} else {
|
||||
qunsetenv("MANGOHUD");
|
||||
qunsetenv("MANGOHUD_DLSYM");
|
||||
}
|
||||
}
|
||||
|
||||
void GameLauncherProvider::applyFilter()
|
||||
{
|
||||
beginResetModel();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class GameLauncherProvider : public QAbstractListModel
|
|||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged)
|
||||
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
|
||||
Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged)
|
||||
|
||||
public:
|
||||
explicit GameLauncherProvider(QObject *parent = nullptr);
|
||||
|
|
@ -46,6 +47,8 @@ public:
|
|||
void setFilterString(const QString &filter);
|
||||
QString sourceFilter() const;
|
||||
void setSourceFilter(const QString &source);
|
||||
bool overlayEnabled() const;
|
||||
void setOverlayEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
Q_INVOKABLE void launch(int index);
|
||||
|
|
@ -57,6 +60,7 @@ Q_SIGNALS:
|
|||
void loadingChanged();
|
||||
void filterStringChanged();
|
||||
void sourceFilterChanged();
|
||||
void overlayEnabledChanged();
|
||||
void gameLaunched(const QString &name);
|
||||
|
||||
private:
|
||||
|
|
@ -88,4 +92,5 @@ private:
|
|||
QString m_sourceFilter; // empty = all, or "desktop"/"steam"/"flatpak"
|
||||
KSharedConfigPtr m_config;
|
||||
bool m_loading = false;
|
||||
bool m_overlayEnabled = false;
|
||||
};
|
||||
|
|
|
|||
64
components/gamingshellplugin/gamemodecontrol.cpp
Normal file
64
components/gamingshellplugin/gamemodecontrol.cpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#include "gamemodecontrol.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusReply>
|
||||
#include <QDebug>
|
||||
|
||||
#include <unistd.h> // getpid()
|
||||
|
||||
static const QString s_service = QStringLiteral("com.feralinteractive.GameMode");
|
||||
static const QString s_path = QStringLiteral("/com/feralinteractive/GameMode");
|
||||
static const QString s_iface = QStringLiteral("com.feralinteractive.GameMode");
|
||||
|
||||
GameModeControl::GameModeControl(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_iface(new QDBusInterface(s_service, s_path, s_iface, QDBusConnection::sessionBus(), this))
|
||||
{
|
||||
m_available = m_iface->isValid();
|
||||
if (!m_available) {
|
||||
qDebug() << "GameModeControl: Feral GameMode not available";
|
||||
}
|
||||
}
|
||||
|
||||
bool GameModeControl::available() const
|
||||
{
|
||||
return m_available;
|
||||
}
|
||||
|
||||
bool GameModeControl::active() const
|
||||
{
|
||||
return m_active;
|
||||
}
|
||||
|
||||
void GameModeControl::requestStart()
|
||||
{
|
||||
if (!m_available || m_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDBusReply<int> reply = m_iface->call(QStringLiteral("RegisterGame"), static_cast<int>(getpid()));
|
||||
if (reply.isValid() && reply.value() == 0) {
|
||||
m_active = true;
|
||||
Q_EMIT activeChanged();
|
||||
} else {
|
||||
qWarning() << "GameModeControl: RegisterGame failed:" << reply.error().message();
|
||||
}
|
||||
}
|
||||
|
||||
void GameModeControl::requestEnd()
|
||||
{
|
||||
if (!m_available || !m_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDBusReply<int> reply = m_iface->call(QStringLiteral("UnregisterGame"), static_cast<int>(getpid()));
|
||||
if (reply.isValid() && reply.value() == 0) {
|
||||
m_active = false;
|
||||
Q_EMIT activeChanged();
|
||||
} else {
|
||||
qWarning() << "GameModeControl: UnregisterGame failed:" << reply.error().message();
|
||||
}
|
||||
}
|
||||
46
components/gamingshellplugin/gamemodecontrol.h
Normal file
46
components/gamingshellplugin/gamemodecontrol.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDBusInterface>
|
||||
#include <QObject>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
/**
|
||||
* D-Bus client for Feral GameMode (com.feralinteractive.GameMode).
|
||||
*
|
||||
* Calling requestStart() tells the daemon to apply performance
|
||||
* optimizations (CPU governor, I/O priority, GPU perf mode, etc.)
|
||||
* for the calling process. requestEnd() reverses them.
|
||||
*
|
||||
* GameMode is optional — if the daemon is not installed the calls
|
||||
* are silently ignored.
|
||||
*/
|
||||
class GameModeControl : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(bool available READ available NOTIFY availableChanged)
|
||||
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
|
||||
|
||||
public:
|
||||
explicit GameModeControl(QObject *parent = nullptr);
|
||||
|
||||
bool available() const;
|
||||
bool active() const;
|
||||
|
||||
Q_INVOKABLE void requestStart();
|
||||
Q_INVOKABLE void requestEnd();
|
||||
|
||||
Q_SIGNALS:
|
||||
void availableChanged();
|
||||
void activeChanged();
|
||||
|
||||
private:
|
||||
QDBusInterface *m_iface = nullptr;
|
||||
bool m_available = false;
|
||||
bool m_active = false;
|
||||
};
|
||||
127
components/gamingshellplugin/powerprofilecontrol.cpp
Normal file
127
components/gamingshellplugin/powerprofilecontrol.cpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#include "powerprofilecontrol.h"
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusVariant>
|
||||
#include <QDebug>
|
||||
|
||||
static const QString s_service = QStringLiteral("net.hadess.PowerProfiles");
|
||||
static const QString s_path = QStringLiteral("/net/hadess/PowerProfiles");
|
||||
static const QString s_iface = QStringLiteral("net.hadess.PowerProfiles");
|
||||
static const QString s_propIface = QStringLiteral("org.freedesktop.DBus.Properties");
|
||||
|
||||
PowerProfileControl::PowerProfileControl(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_iface(new QDBusInterface(s_service, s_path, s_iface, QDBusConnection::systemBus(), this))
|
||||
{
|
||||
if (m_iface->isValid()) {
|
||||
m_available = true;
|
||||
fetchState();
|
||||
|
||||
// Subscribe to property changes
|
||||
QDBusConnection::systemBus()
|
||||
.connect(s_service, s_path, s_propIface, QStringLiteral("PropertiesChanged"), this, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList)));
|
||||
} else {
|
||||
qDebug() << "PowerProfileControl: power-profiles-daemon not available";
|
||||
}
|
||||
}
|
||||
|
||||
QString PowerProfileControl::activeProfile() const
|
||||
{
|
||||
return m_activeProfile;
|
||||
}
|
||||
|
||||
void PowerProfileControl::setActiveProfile(const QString &profile)
|
||||
{
|
||||
if (!m_available || profile == m_activeProfile) {
|
||||
return;
|
||||
}
|
||||
if (!m_profiles.contains(profile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Write via org.freedesktop.DBus.Properties.Set
|
||||
QDBusInterface propIface(s_service, s_path, s_propIface, QDBusConnection::systemBus());
|
||||
propIface.call(QStringLiteral("Set"), s_iface, QStringLiteral("ActiveProfile"), QVariant::fromValue(QDBusVariant(profile)));
|
||||
}
|
||||
|
||||
QStringList PowerProfileControl::profiles() const
|
||||
{
|
||||
return m_profiles;
|
||||
}
|
||||
|
||||
bool PowerProfileControl::available() const
|
||||
{
|
||||
return m_available;
|
||||
}
|
||||
|
||||
void PowerProfileControl::fetchState()
|
||||
{
|
||||
// Read ActiveProfile
|
||||
QDBusInterface propIface(s_service, s_path, s_propIface, QDBusConnection::systemBus());
|
||||
|
||||
QDBusReply<QDBusVariant> profileReply = propIface.call(QStringLiteral("Get"), s_iface, QStringLiteral("ActiveProfile"));
|
||||
if (profileReply.isValid()) {
|
||||
const QString profile = profileReply.value().variant().toString();
|
||||
if (profile != m_activeProfile) {
|
||||
m_activeProfile = profile;
|
||||
Q_EMIT activeProfileChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Read Profiles — array of dicts, each with a "Profile" key
|
||||
QDBusReply<QDBusVariant> profilesReply = propIface.call(QStringLiteral("Get"), s_iface, QStringLiteral("Profiles"));
|
||||
if (profilesReply.isValid()) {
|
||||
QStringList profiles;
|
||||
const QVariant profilesVariant = profilesReply.value().variant();
|
||||
if (profilesVariant.canConvert<QVariantList>()) {
|
||||
const QVariantList list = profilesVariant.toList();
|
||||
for (const QVariant &item : list) {
|
||||
const QVariantMap map = item.toMap();
|
||||
QString profileName;
|
||||
if (map.contains(QStringLiteral("Profile"))) {
|
||||
QVariant value = map.value(QStringLiteral("Profile"));
|
||||
if (value.canConvert<QDBusVariant>()) {
|
||||
value = value.value<QDBusVariant>().variant();
|
||||
}
|
||||
profileName = value.toString();
|
||||
}
|
||||
if (!profileName.isEmpty()) {
|
||||
profiles.append(profileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (profiles != m_profiles) {
|
||||
m_profiles = profiles;
|
||||
Q_EMIT profilesChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PowerProfileControl::onPropertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated)
|
||||
{
|
||||
Q_UNUSED(invalidated)
|
||||
if (interface != s_iface) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changed.contains(QStringLiteral("ActiveProfile"))) {
|
||||
QVariant value = changed.value(QStringLiteral("ActiveProfile"));
|
||||
if (value.canConvert<QDBusVariant>()) {
|
||||
value = value.value<QDBusVariant>().variant();
|
||||
}
|
||||
const QString profile = value.toString();
|
||||
if (profile != m_activeProfile) {
|
||||
m_activeProfile = profile;
|
||||
Q_EMIT activeProfileChanged();
|
||||
}
|
||||
}
|
||||
|
||||
if (changed.contains(QStringLiteral("Profiles"))) {
|
||||
fetchState();
|
||||
}
|
||||
}
|
||||
44
components/gamingshellplugin/powerprofilecontrol.h
Normal file
44
components/gamingshellplugin/powerprofilecontrol.h
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDBusInterface>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
class PowerProfileControl : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(QString activeProfile READ activeProfile WRITE setActiveProfile NOTIFY activeProfileChanged)
|
||||
Q_PROPERTY(QStringList profiles READ profiles NOTIFY profilesChanged)
|
||||
Q_PROPERTY(bool available READ available NOTIFY availableChanged)
|
||||
|
||||
public:
|
||||
explicit PowerProfileControl(QObject *parent = nullptr);
|
||||
|
||||
QString activeProfile() const;
|
||||
void setActiveProfile(const QString &profile);
|
||||
QStringList profiles() const;
|
||||
bool available() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void activeProfileChanged();
|
||||
void profilesChanged();
|
||||
void availableChanged();
|
||||
|
||||
private Q_SLOTS:
|
||||
void onPropertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated);
|
||||
|
||||
private:
|
||||
void fetchState();
|
||||
|
||||
QDBusInterface *m_iface = nullptr;
|
||||
QString m_activeProfile;
|
||||
QStringList m_profiles;
|
||||
bool m_available = false;
|
||||
};
|
||||
|
|
@ -40,6 +40,44 @@ ContainmentItem {
|
|||
property bool gameCenterOpen: ShellSettings.Settings.gamingModeEnabled
|
||||
property bool showGameCenterHint: false
|
||||
|
||||
// State saved when gaming mode activates, restored when it deactivates
|
||||
property string _savedPowerProfile: ""
|
||||
property bool _savedDnd: false
|
||||
property bool _gamingSessionActive: false
|
||||
|
||||
function _applyGamingModeState(enabled) {
|
||||
root.gameCenterOpen = enabled
|
||||
GamingShell.GamepadManager.active = enabled
|
||||
|
||||
if (enabled === root._gamingSessionActive) {
|
||||
return
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
// Save current state and apply gaming optimizations
|
||||
root._savedDnd = MobileShellState.ShellDBusClient.doNotDisturb
|
||||
MobileShellState.ShellDBusClient.doNotDisturb = true
|
||||
|
||||
if (GamingShell.PowerProfileControl.available) {
|
||||
root._savedPowerProfile = GamingShell.PowerProfileControl.activeProfile
|
||||
GamingShell.PowerProfileControl.activeProfile = "performance"
|
||||
}
|
||||
|
||||
GamingShell.GameModeControl.requestStart()
|
||||
root._gamingSessionActive = true
|
||||
} else {
|
||||
// Restore previous state
|
||||
MobileShellState.ShellDBusClient.doNotDisturb = root._savedDnd
|
||||
|
||||
if (GamingShell.PowerProfileControl.available && root._savedPowerProfile.length > 0) {
|
||||
GamingShell.PowerProfileControl.activeProfile = root._savedPowerProfile
|
||||
}
|
||||
|
||||
GamingShell.GameModeControl.requestEnd()
|
||||
root._gamingSessionActive = false
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: gameCenterHintTimer
|
||||
interval: 2600
|
||||
|
|
@ -49,8 +87,7 @@ ContainmentItem {
|
|||
Connections {
|
||||
target: ShellSettings.Settings
|
||||
function onGamingModeEnabledChanged() {
|
||||
root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled
|
||||
GamingShell.GamepadManager.active = ShellSettings.Settings.gamingModeEnabled
|
||||
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +103,7 @@ ContainmentItem {
|
|||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled
|
||||
GamingShell.GamepadManager.active = ShellSettings.Settings.gamingModeEnabled
|
||||
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
|
||||
folio.FolioSettings.load();
|
||||
folio.FavouritesModel.load();
|
||||
folio.PageListModel.load();
|
||||
|
|
|
|||
Loading…
Reference in a new issue