mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 07:03:08 +00:00
Add gamingshellplugin with SDL3 and game library
SDL3-backed gamepad manager polls at 60Hz, handles hotplug, exposes battery/rumble/LED per device. Game launcher aggregates XDG desktop entries, Steam appmanifests, and Flatpak into a single sorted model. Game Center uses the new model instead of folio's search model. D-pad/A/B/Y navigate and launch. Guide button toggles the overlay. Battery and controller status shown in the HUD.
This commit is contained in:
parent
70fa861b30
commit
1bf8cfb2ba
12 changed files with 1116 additions and 26 deletions
|
|
@ -112,6 +112,7 @@ set_package_properties(PlasmaWaylandProtocols PROPERTIES
|
|||
|
||||
find_package(LibKWorkspace CONFIG REQUIRED)
|
||||
find_package(Libudev REQUIRED)
|
||||
find_package(SDL3 REQUIRED)
|
||||
|
||||
find_package(KWinDBusInterface)
|
||||
set_package_properties(KWinDBusInterface PROPERTIES DESCRIPTION "KWin DBus interface"
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ add_subdirectory(wallpaperimageplugin)
|
|||
add_subdirectory(dpmsplugin)
|
||||
add_subdirectory(screenbrightnessplugin)
|
||||
add_subdirectory(raiselockscreenplugin)
|
||||
add_subdirectory(gamingshellplugin)
|
||||
|
|
|
|||
24
components/gamingshellplugin/CMakeLists.txt
Normal file
24
components/gamingshellplugin/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
find_package(SDL3 REQUIRED)
|
||||
|
||||
ecm_add_qml_module(gamingshellplugin URI org.kde.plasma.private.mobileshell.gamingshellplugin GENERATE_PLUGIN_SOURCE)
|
||||
|
||||
target_sources(gamingshellplugin PRIVATE
|
||||
gamepadmanager.cpp
|
||||
gamepaddevice.cpp
|
||||
gamelauncherprovider.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(gamingshellplugin PRIVATE
|
||||
Qt::Core
|
||||
Qt::Qml
|
||||
Qt::Quick
|
||||
KF6::I18n
|
||||
KF6::Service
|
||||
KF6::CoreAddons
|
||||
SDL3::SDL3
|
||||
)
|
||||
|
||||
ecm_finalize_qml_module(gamingshellplugin)
|
||||
284
components/gamingshellplugin/gamelauncherprovider.cpp
Normal file
284
components/gamingshellplugin/gamelauncherprovider.cpp
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#include "gamelauncherprovider.h"
|
||||
|
||||
#include <KService>
|
||||
#include <KShell>
|
||||
#include <KSycoca>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
||||
GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(KSycoca::self(), &KSycoca::databaseChanged, this, &GameLauncherProvider::refresh);
|
||||
refresh();
|
||||
}
|
||||
|
||||
int GameLauncherProvider::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : m_games.size();
|
||||
}
|
||||
|
||||
QVariant GameLauncherProvider::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_games.size()) {
|
||||
return {};
|
||||
}
|
||||
const auto &g = m_games.at(index.row());
|
||||
switch (role) {
|
||||
case NameRole:
|
||||
return g.name;
|
||||
case IconRole:
|
||||
return g.icon;
|
||||
case SourceRole:
|
||||
return g.source;
|
||||
case StorageIdRole:
|
||||
return g.storageId;
|
||||
case LaunchCommandRole:
|
||||
return g.launchCommand;
|
||||
case ArtworkRole:
|
||||
return g.artwork;
|
||||
case InstalledRole:
|
||||
return g.installed;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> GameLauncherProvider::roleNames() const
|
||||
{
|
||||
return {
|
||||
{NameRole, "name"},
|
||||
{IconRole, "icon"},
|
||||
{SourceRole, "source"},
|
||||
{StorageIdRole, "storageId"},
|
||||
{LaunchCommandRole, "launchCommand"},
|
||||
{ArtworkRole, "artwork"},
|
||||
{InstalledRole, "installed"},
|
||||
};
|
||||
}
|
||||
|
||||
int GameLauncherProvider::count() const
|
||||
{
|
||||
return m_games.size();
|
||||
}
|
||||
|
||||
bool GameLauncherProvider::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void GameLauncherProvider::refresh()
|
||||
{
|
||||
m_loading = true;
|
||||
Q_EMIT loadingChanged();
|
||||
|
||||
beginResetModel();
|
||||
m_games.clear();
|
||||
|
||||
loadDesktopGames();
|
||||
loadSteamGames();
|
||||
loadFlatpakGames();
|
||||
|
||||
// Sort alphabetically, case-insensitive
|
||||
std::sort(m_games.begin(), m_games.end(), [](const GameEntry &a, const GameEntry &b) {
|
||||
return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
|
||||
endResetModel();
|
||||
|
||||
m_loading = false;
|
||||
Q_EMIT loadingChanged();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
||||
void GameLauncherProvider::launch(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_games.size()) {
|
||||
return;
|
||||
}
|
||||
const auto &g = m_games.at(index);
|
||||
|
||||
if (g.source == QLatin1String("desktop")) {
|
||||
// Launch via KService for proper activation tracking
|
||||
auto service = KService::serviceByStorageId(g.storageId);
|
||||
if (service) {
|
||||
// Use QProcess to launch the exec line — KIO::ApplicationLauncherJob
|
||||
// would be better but requires KIOWidgets which is heavy for a plugin.
|
||||
QStringList args = KShell::splitArgs(service->exec());
|
||||
if (!args.isEmpty()) {
|
||||
QString program = args.takeFirst();
|
||||
QProcess::startDetached(program, args);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Steam, Flatpak, etc. — run the launch command directly
|
||||
QStringList parts = g.launchCommand.split(QLatin1Char(' '));
|
||||
if (!parts.isEmpty()) {
|
||||
QString program = parts.takeFirst();
|
||||
QProcess::startDetached(program, parts);
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT gameLaunched(g.name);
|
||||
}
|
||||
|
||||
// --- XDG .desktop games ---
|
||||
|
||||
void GameLauncherProvider::loadDesktopGames()
|
||||
{
|
||||
const auto services = KService::allServices();
|
||||
for (const auto &service : services) {
|
||||
if (service->noDisplay() || service->exec().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const QStringList cats = service->categories();
|
||||
bool isGame = false;
|
||||
for (const auto &cat : cats) {
|
||||
if (cat.compare(QLatin1String("Game"), Qt::CaseInsensitive) == 0) {
|
||||
isGame = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isGame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GameEntry entry;
|
||||
entry.name = service->name();
|
||||
entry.icon = service->icon();
|
||||
entry.source = QStringLiteral("desktop");
|
||||
entry.storageId = service->storageId();
|
||||
entry.launchCommand = service->exec();
|
||||
entry.installed = true;
|
||||
m_games.append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Steam library ---
|
||||
|
||||
void GameLauncherProvider::loadSteamGames()
|
||||
{
|
||||
// Look for Steam library folders
|
||||
const QStringList steamRoots = {
|
||||
QDir::homePath() + QStringLiteral("/.steam/steam"),
|
||||
QDir::homePath() + QStringLiteral("/.local/share/Steam"),
|
||||
// Flatpak Steam
|
||||
QDir::homePath() + QStringLiteral("/.var/app/com.valvesoftware.Steam/.steam/steam"),
|
||||
QDir::homePath() + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
|
||||
};
|
||||
|
||||
QStringList libraryPaths;
|
||||
for (const auto &root : steamRoots) {
|
||||
const QString vdfPath = root + QStringLiteral("/steamapps/libraryfolders.vdf");
|
||||
QFile vdf(vdfPath);
|
||||
if (!vdf.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
// Simple parse: look for "path" lines
|
||||
static const QRegularExpression pathRe(QStringLiteral("\"path\"\\s+\"([^\"]+)\""));
|
||||
QTextStream stream(&vdf);
|
||||
while (!stream.atEnd()) {
|
||||
const QString line = stream.readLine();
|
||||
auto match = pathRe.match(line);
|
||||
if (match.hasMatch()) {
|
||||
libraryPaths.append(match.captured(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan each library path for appmanifest_*.acf
|
||||
static const QRegularExpression nameRe(QStringLiteral("\"name\"\\s+\"([^\"]+)\""));
|
||||
static const QRegularExpression appidRe(QStringLiteral("\"appid\"\\s+\"(\\d+)\""));
|
||||
|
||||
for (const auto &libPath : std::as_const(libraryPaths)) {
|
||||
QDir steamapps(libPath + QStringLiteral("/steamapps"));
|
||||
if (!steamapps.exists()) {
|
||||
continue;
|
||||
}
|
||||
const auto manifests = steamapps.entryList({QStringLiteral("appmanifest_*.acf")}, QDir::Files);
|
||||
for (const auto &manifest : manifests) {
|
||||
QFile f(steamapps.filePath(manifest));
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
QString appName;
|
||||
QString appId;
|
||||
QTextStream ts(&f);
|
||||
while (!ts.atEnd()) {
|
||||
const QString line = ts.readLine();
|
||||
if (appName.isEmpty()) {
|
||||
auto m = nameRe.match(line);
|
||||
if (m.hasMatch()) {
|
||||
appName = m.captured(1);
|
||||
}
|
||||
}
|
||||
if (appId.isEmpty()) {
|
||||
auto m = appidRe.match(line);
|
||||
if (m.hasMatch()) {
|
||||
appId = m.captured(1);
|
||||
}
|
||||
}
|
||||
if (!appName.isEmpty() && !appId.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (appName.isEmpty() || appId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip Steamworks Common Redistributables and Proton/tools
|
||||
if (appId == QLatin1String("228980")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GameEntry entry;
|
||||
entry.name = appName;
|
||||
entry.icon = QStringLiteral("steam");
|
||||
entry.source = QStringLiteral("steam");
|
||||
entry.storageId = QStringLiteral("steam://rungameid/") + appId;
|
||||
entry.launchCommand = QStringLiteral("steam steam://rungameid/") + appId;
|
||||
entry.installed = true;
|
||||
|
||||
// Check for grid artwork
|
||||
for (const auto &root : steamRoots) {
|
||||
const QString gridDir = root + QStringLiteral("/appcache/librarycache/") + appId;
|
||||
const QStringList artSuffixes = {
|
||||
QStringLiteral("_library_600x900.jpg"),
|
||||
QStringLiteral("_header.jpg"),
|
||||
};
|
||||
for (const auto &suffix : artSuffixes) {
|
||||
const QString artPath = gridDir + suffix;
|
||||
if (QFile::exists(artPath)) {
|
||||
entry.artwork = artPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!entry.artwork.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_games.append(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Flatpak games (non-Steam) ---
|
||||
|
||||
void GameLauncherProvider::loadFlatpakGames()
|
||||
{
|
||||
// Flatpak games that export .desktop files with Game category
|
||||
// are already picked up by loadDesktopGames() via KService.
|
||||
// This method is a hook for future Flatpak-specific enrichment
|
||||
// (e.g. querying flatpak metadata for games that don't set
|
||||
// the Game category properly).
|
||||
}
|
||||
66
components/gamingshellplugin/gamelauncherprovider.h
Normal file
66
components/gamingshellplugin/gamelauncherprovider.h
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
class GameLauncherProvider : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
explicit GameLauncherProvider(QObject *parent = nullptr);
|
||||
|
||||
enum Roles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
IconRole,
|
||||
SourceRole, // "desktop", "steam", "flatpak"
|
||||
StorageIdRole, // .desktop file name or launch URI
|
||||
LaunchCommandRole,
|
||||
ArtworkRole, // path to banner/grid image (empty if none)
|
||||
InstalledRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
int count() const;
|
||||
bool loading() const;
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
Q_INVOKABLE void launch(int index);
|
||||
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
void loadingChanged();
|
||||
void gameLaunched(const QString &name);
|
||||
|
||||
private:
|
||||
struct GameEntry {
|
||||
QString name;
|
||||
QString icon;
|
||||
QString source;
|
||||
QString storageId;
|
||||
QString launchCommand;
|
||||
QString artwork;
|
||||
bool installed = true;
|
||||
};
|
||||
|
||||
void loadDesktopGames();
|
||||
void loadSteamGames();
|
||||
void loadFlatpakGames();
|
||||
|
||||
QList<GameEntry> m_games;
|
||||
bool m_loading = false;
|
||||
};
|
||||
135
components/gamingshellplugin/gamepaddevice.cpp
Normal file
135
components/gamingshellplugin/gamepaddevice.cpp
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#include "gamepaddevice.h"
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_power.h>
|
||||
#include <SDL3/SDL_properties.h>
|
||||
|
||||
GamepadDevice::GamepadDevice(SDL_Gamepad *pad, int id, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_pad(pad)
|
||||
, m_id(id)
|
||||
{
|
||||
refreshBattery();
|
||||
}
|
||||
|
||||
GamepadDevice::~GamepadDevice()
|
||||
{
|
||||
if (m_pad) {
|
||||
SDL_CloseGamepad(m_pad);
|
||||
m_pad = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int GamepadDevice::deviceId() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
QString GamepadDevice::name() const
|
||||
{
|
||||
if (!m_pad) {
|
||||
return {};
|
||||
}
|
||||
const char *n = SDL_GetGamepadName(m_pad);
|
||||
return n ? QString::fromUtf8(n) : QString();
|
||||
}
|
||||
|
||||
QString GamepadDevice::type() const
|
||||
{
|
||||
if (!m_pad) {
|
||||
return QStringLiteral("unknown");
|
||||
}
|
||||
switch (SDL_GetGamepadType(m_pad)) {
|
||||
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||
case SDL_GAMEPAD_TYPE_XBOXONE:
|
||||
return QStringLiteral("xbox");
|
||||
case SDL_GAMEPAD_TYPE_PS3:
|
||||
case SDL_GAMEPAD_TYPE_PS4:
|
||||
case SDL_GAMEPAD_TYPE_PS5:
|
||||
return QStringLiteral("playstation");
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
|
||||
return QStringLiteral("nintendo");
|
||||
default:
|
||||
return QStringLiteral("generic");
|
||||
}
|
||||
}
|
||||
|
||||
int GamepadDevice::batteryPercent() const
|
||||
{
|
||||
return m_batteryPercent;
|
||||
}
|
||||
|
||||
bool GamepadDevice::hasRumble() const
|
||||
{
|
||||
if (!m_pad) {
|
||||
return false;
|
||||
}
|
||||
SDL_PropertiesID props = SDL_GetGamepadProperties(m_pad);
|
||||
return SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false);
|
||||
}
|
||||
|
||||
bool GamepadDevice::hasLED() const
|
||||
{
|
||||
if (!m_pad) {
|
||||
return false;
|
||||
}
|
||||
SDL_PropertiesID props = SDL_GetGamepadProperties(m_pad);
|
||||
return SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
|
||||
}
|
||||
|
||||
int GamepadDevice::playerIndex() const
|
||||
{
|
||||
if (!m_pad) {
|
||||
return -1;
|
||||
}
|
||||
return SDL_GetGamepadPlayerIndex(m_pad);
|
||||
}
|
||||
|
||||
void GamepadDevice::setPlayerIndex(int index)
|
||||
{
|
||||
if (!m_pad) {
|
||||
return;
|
||||
}
|
||||
SDL_SetGamepadPlayerIndex(m_pad, index);
|
||||
Q_EMIT playerIndexChanged();
|
||||
}
|
||||
|
||||
bool GamepadDevice::rumble(int lowFreqMs, int highFreqMs, int durationMs)
|
||||
{
|
||||
if (!m_pad) {
|
||||
return false;
|
||||
}
|
||||
return SDL_RumbleGamepad(m_pad, static_cast<uint16_t>(lowFreqMs), static_cast<uint16_t>(highFreqMs), static_cast<uint32_t>(durationMs));
|
||||
}
|
||||
|
||||
bool GamepadDevice::setLED(int r, int g, int b)
|
||||
{
|
||||
if (!m_pad) {
|
||||
return false;
|
||||
}
|
||||
return SDL_SetGamepadLED(m_pad, static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
||||
}
|
||||
|
||||
SDL_Gamepad *GamepadDevice::sdlGamepad() const
|
||||
{
|
||||
return m_pad;
|
||||
}
|
||||
|
||||
void GamepadDevice::refreshBattery()
|
||||
{
|
||||
if (!m_pad) {
|
||||
return;
|
||||
}
|
||||
int pct = -1;
|
||||
SDL_GetGamepadPowerInfo(m_pad, &pct);
|
||||
if (pct != m_batteryPercent) {
|
||||
m_batteryPercent = pct;
|
||||
Q_EMIT batteryPercentChanged();
|
||||
}
|
||||
}
|
||||
53
components/gamingshellplugin/gamepaddevice.h
Normal file
53
components/gamingshellplugin/gamepaddevice.h
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
struct SDL_Gamepad;
|
||||
|
||||
class GamepadDevice : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
Q_PROPERTY(int deviceId READ deviceId CONSTANT)
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
Q_PROPERTY(QString type READ type CONSTANT)
|
||||
Q_PROPERTY(int batteryPercent READ batteryPercent NOTIFY batteryPercentChanged)
|
||||
Q_PROPERTY(bool hasRumble READ hasRumble CONSTANT)
|
||||
Q_PROPERTY(bool hasLED READ hasLED CONSTANT)
|
||||
Q_PROPERTY(int playerIndex READ playerIndex WRITE setPlayerIndex NOTIFY playerIndexChanged)
|
||||
|
||||
public:
|
||||
explicit GamepadDevice(SDL_Gamepad *pad, int id, QObject *parent = nullptr);
|
||||
~GamepadDevice() override;
|
||||
|
||||
int deviceId() const;
|
||||
QString name() const;
|
||||
QString type() const;
|
||||
int batteryPercent() const;
|
||||
bool hasRumble() const;
|
||||
bool hasLED() const;
|
||||
int playerIndex() const;
|
||||
void setPlayerIndex(int index);
|
||||
|
||||
Q_INVOKABLE bool rumble(int lowFreqMs, int highFreqMs, int durationMs);
|
||||
Q_INVOKABLE bool setLED(int r, int g, int b);
|
||||
|
||||
SDL_Gamepad *sdlGamepad() const;
|
||||
void refreshBattery();
|
||||
|
||||
Q_SIGNALS:
|
||||
void batteryPercentChanged();
|
||||
void playerIndexChanged();
|
||||
|
||||
private:
|
||||
SDL_Gamepad *m_pad = nullptr;
|
||||
int m_id = 0;
|
||||
int m_batteryPercent = -1;
|
||||
};
|
||||
261
components/gamingshellplugin/gamepadmanager.cpp
Normal file
261
components/gamingshellplugin/gamepadmanager.cpp
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#include "gamepadmanager.h"
|
||||
#include "gamepaddevice.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
GamepadManager::GamepadManager(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
m_pollTimer.setInterval(16); // ~60 Hz
|
||||
connect(&m_pollTimer, &QTimer::timeout, this, &GamepadManager::poll);
|
||||
}
|
||||
|
||||
GamepadManager::~GamepadManager()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
// --- QAbstractListModel ---
|
||||
|
||||
int GamepadManager::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : m_gamepads.size();
|
||||
}
|
||||
|
||||
QVariant GamepadManager::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_gamepads.size()) {
|
||||
return {};
|
||||
}
|
||||
auto *dev = m_gamepads.at(index.row());
|
||||
switch (role) {
|
||||
case DeviceRole:
|
||||
return QVariant::fromValue(dev);
|
||||
case NameRole:
|
||||
return dev->name();
|
||||
case TypeRole:
|
||||
return dev->type();
|
||||
case BatteryRole:
|
||||
return dev->batteryPercent();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> GamepadManager::roleNames() const
|
||||
{
|
||||
return {
|
||||
{DeviceRole, "device"},
|
||||
{NameRole, "name"},
|
||||
{TypeRole, "type"},
|
||||
{BatteryRole, "battery"},
|
||||
};
|
||||
}
|
||||
|
||||
// --- Properties ---
|
||||
|
||||
bool GamepadManager::active() const
|
||||
{
|
||||
return m_active;
|
||||
}
|
||||
|
||||
void GamepadManager::setActive(bool active)
|
||||
{
|
||||
if (m_active == active) {
|
||||
return;
|
||||
}
|
||||
m_active = active;
|
||||
if (active) {
|
||||
start();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
Q_EMIT activeChanged();
|
||||
}
|
||||
|
||||
int GamepadManager::count() const
|
||||
{
|
||||
return m_gamepads.size();
|
||||
}
|
||||
|
||||
bool GamepadManager::hasGamepad() const
|
||||
{
|
||||
return !m_gamepads.isEmpty();
|
||||
}
|
||||
|
||||
GamepadDevice *GamepadManager::primaryGamepad() const
|
||||
{
|
||||
return m_gamepads.isEmpty() ? nullptr : m_gamepads.first();
|
||||
}
|
||||
|
||||
GamepadDevice *GamepadManager::gamepadAt(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_gamepads.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return m_gamepads.at(index);
|
||||
}
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
void GamepadManager::start()
|
||||
{
|
||||
if (m_sdlInitialized) {
|
||||
return;
|
||||
}
|
||||
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
|
||||
qWarning() << "GamepadManager: SDL_Init failed:" << SDL_GetError();
|
||||
return;
|
||||
}
|
||||
m_sdlInitialized = true;
|
||||
|
||||
// Enumerate already-connected gamepads
|
||||
int count = 0;
|
||||
SDL_JoystickID *ids = SDL_GetGamepads(&count);
|
||||
if (ids) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
addGamepad(ids[i]);
|
||||
}
|
||||
SDL_free(ids);
|
||||
}
|
||||
|
||||
m_pollTimer.start();
|
||||
}
|
||||
|
||||
void GamepadManager::stop()
|
||||
{
|
||||
m_pollTimer.stop();
|
||||
|
||||
if (!m_gamepads.isEmpty()) {
|
||||
beginResetModel();
|
||||
qDeleteAll(m_gamepads);
|
||||
m_gamepads.clear();
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
Q_EMIT primaryGamepadChanged();
|
||||
}
|
||||
|
||||
if (m_sdlInitialized) {
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
m_sdlInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Event polling ---
|
||||
|
||||
void GamepadManager::poll()
|
||||
{
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
addGamepad(event.gdevice.which);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
removeGamepad(event.gdevice.which);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN: {
|
||||
int idx = indexForInstanceId(event.gbutton.which);
|
||||
if (idx >= 0) {
|
||||
Q_EMIT buttonPressed(event.gbutton.button, idx);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP: {
|
||||
int idx = indexForInstanceId(event.gbutton.which);
|
||||
if (idx >= 0) {
|
||||
Q_EMIT buttonReleased(event.gbutton.button, idx);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
|
||||
int idx = indexForInstanceId(event.gaxis.which);
|
||||
if (idx >= 0) {
|
||||
float normalized = static_cast<float>(event.gaxis.value) / 32767.0f;
|
||||
Q_EMIT axisChanged(event.gaxis.axis, normalized, idx);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh battery state periodically (every ~5 seconds = 300 frames)
|
||||
static int batteryCounter = 0;
|
||||
if (++batteryCounter >= 300) {
|
||||
batteryCounter = 0;
|
||||
for (auto *dev : std::as_const(m_gamepads)) {
|
||||
dev->refreshBattery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hotplug ---
|
||||
|
||||
void GamepadManager::addGamepad(int instanceId)
|
||||
{
|
||||
// Already tracked?
|
||||
if (indexForInstanceId(instanceId) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Gamepad *pad = SDL_OpenGamepad(instanceId);
|
||||
if (!pad) {
|
||||
qWarning() << "GamepadManager: failed to open gamepad" << instanceId << SDL_GetError();
|
||||
return;
|
||||
}
|
||||
|
||||
auto *device = new GamepadDevice(pad, instanceId, this);
|
||||
int row = m_gamepads.size();
|
||||
beginInsertRows(QModelIndex(), row, row);
|
||||
m_gamepads.append(device);
|
||||
endInsertRows();
|
||||
|
||||
Q_EMIT countChanged();
|
||||
if (m_gamepads.size() == 1) {
|
||||
Q_EMIT primaryGamepadChanged();
|
||||
}
|
||||
|
||||
qDebug() << "GamepadManager: connected" << device->name() << "(" << device->type() << ")";
|
||||
}
|
||||
|
||||
void GamepadManager::removeGamepad(int instanceId)
|
||||
{
|
||||
int idx = indexForInstanceId(instanceId);
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginRemoveRows(QModelIndex(), idx, idx);
|
||||
auto *dev = m_gamepads.takeAt(idx);
|
||||
endRemoveRows();
|
||||
|
||||
qDebug() << "GamepadManager: disconnected" << dev->name();
|
||||
dev->deleteLater();
|
||||
|
||||
Q_EMIT countChanged();
|
||||
if (idx == 0) {
|
||||
Q_EMIT primaryGamepadChanged();
|
||||
}
|
||||
}
|
||||
|
||||
int GamepadManager::indexForInstanceId(int instanceId) const
|
||||
{
|
||||
for (int i = 0; i < m_gamepads.size(); ++i) {
|
||||
if (m_gamepads.at(i)->deviceId() == instanceId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
89
components/gamingshellplugin/gamepadmanager.h
Normal file
89
components/gamingshellplugin/gamepadmanager.h
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QTimer>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
class GamepadDevice;
|
||||
|
||||
class GamepadManager : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged)
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
Q_PROPERTY(GamepadDevice *primaryGamepad READ primaryGamepad NOTIFY primaryGamepadChanged)
|
||||
Q_PROPERTY(bool hasGamepad READ hasGamepad NOTIFY countChanged)
|
||||
|
||||
public:
|
||||
explicit GamepadManager(QObject *parent = nullptr);
|
||||
~GamepadManager() override;
|
||||
|
||||
enum Roles {
|
||||
DeviceRole = Qt::UserRole + 1,
|
||||
NameRole,
|
||||
TypeRole,
|
||||
BatteryRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
// Buttons matching SDL_GamepadButton, re-exported for QML
|
||||
enum Button {
|
||||
ButtonA,
|
||||
ButtonB,
|
||||
ButtonX,
|
||||
ButtonY,
|
||||
ButtonBack,
|
||||
ButtonGuide,
|
||||
ButtonStart,
|
||||
ButtonLeftStick,
|
||||
ButtonRightStick,
|
||||
ButtonLeftShoulder,
|
||||
ButtonRightShoulder,
|
||||
ButtonDPadUp,
|
||||
ButtonDPadDown,
|
||||
ButtonDPadLeft,
|
||||
ButtonDPadRight,
|
||||
ButtonMisc1,
|
||||
};
|
||||
Q_ENUM(Button)
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
bool active() const;
|
||||
void setActive(bool active);
|
||||
int count() const;
|
||||
bool hasGamepad() const;
|
||||
GamepadDevice *primaryGamepad() const;
|
||||
|
||||
Q_INVOKABLE GamepadDevice *gamepadAt(int index) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void activeChanged();
|
||||
void countChanged();
|
||||
void primaryGamepadChanged();
|
||||
|
||||
void buttonPressed(int button, int gamepadIndex);
|
||||
void buttonReleased(int button, int gamepadIndex);
|
||||
void axisChanged(int axis, float value, int gamepadIndex);
|
||||
|
||||
private:
|
||||
void start();
|
||||
void stop();
|
||||
void poll();
|
||||
void addGamepad(int instanceId);
|
||||
void removeGamepad(int instanceId);
|
||||
int indexForInstanceId(int instanceId) const;
|
||||
|
||||
bool m_active = false;
|
||||
bool m_sdlInitialized = false;
|
||||
QTimer m_pollTimer;
|
||||
QList<GamepadDevice *> m_gamepads;
|
||||
};
|
||||
|
|
@ -11,6 +11,7 @@ import org.kde.plasma.components 3.0 as PC3
|
|||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
||||
import org.kde.layershell 1.0 as LayerShell
|
||||
|
||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||
|
|
@ -48,7 +49,7 @@ Window {
|
|||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
root.folio.ApplicationListSearchModel.categoryFilter = "Game"
|
||||
GamingShell.GameLauncherProvider.refresh()
|
||||
if (runningGames.hasTasks) {
|
||||
runningGames.focusFirstTask()
|
||||
} else {
|
||||
|
|
@ -57,6 +58,53 @@ Window {
|
|||
}
|
||||
}
|
||||
|
||||
// Translate gamepad buttons to focus navigation
|
||||
Connections {
|
||||
target: GamingShell.GamepadManager
|
||||
enabled: root.visible
|
||||
|
||||
function onButtonPressed(button, gamepadIndex) {
|
||||
switch (button) {
|
||||
case GamingShell.GamepadManager.ButtonDPadUp:
|
||||
if (grid.activeFocus) {
|
||||
if (grid.currentIndex < grid.columns && runningGames.hasTasks) {
|
||||
runningGames.focusFirstTask()
|
||||
} else {
|
||||
grid.moveCurrentIndexUp()
|
||||
}
|
||||
}
|
||||
break
|
||||
case GamingShell.GamepadManager.ButtonDPadDown:
|
||||
if (taskList.activeFocus || runningGames.activeFocus) {
|
||||
grid.forceActiveFocus()
|
||||
} else if (grid.activeFocus) {
|
||||
grid.moveCurrentIndexDown()
|
||||
}
|
||||
break
|
||||
case GamingShell.GamepadManager.ButtonDPadLeft:
|
||||
if (grid.activeFocus) grid.moveCurrentIndexLeft()
|
||||
break
|
||||
case GamingShell.GamepadManager.ButtonDPadRight:
|
||||
if (grid.activeFocus) grid.moveCurrentIndexRight()
|
||||
break
|
||||
case GamingShell.GamepadManager.ButtonA:
|
||||
if (grid.activeFocus && grid.currentItem) {
|
||||
GamingShell.GameLauncherProvider.launch(grid.currentIndex)
|
||||
root.gameStarted()
|
||||
} else if (taskList.activeFocus && taskList.currentItem) {
|
||||
taskList.currentItem.activate()
|
||||
}
|
||||
break
|
||||
case GamingShell.GamepadManager.ButtonB:
|
||||
root.dismissRequested()
|
||||
break
|
||||
case GamingShell.GamepadManager.ButtonY:
|
||||
root.requestExitGamingMode()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
Kirigami.Theme.inherit: false
|
||||
|
|
@ -119,11 +167,7 @@ Window {
|
|||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
// Filter the app list to the Games XDG category.
|
||||
// ApplicationListSearchModel.categoryFilter is declared in
|
||||
// applicationlistmodel.h and filters on the CategoriesRole of
|
||||
// ApplicationListModel.
|
||||
model: root.folio.ApplicationListSearchModel
|
||||
model: GamingShell.GameLauncherProvider
|
||||
|
||||
readonly property real minCellSize: Kirigami.Units.gridUnit * 7
|
||||
readonly property int columns: Math.max(2, Math.floor(width / minCellSize))
|
||||
|
|
@ -147,23 +191,129 @@ Window {
|
|||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: currentItem && currentItem.launch()
|
||||
Keys.onEnterPressed: currentItem && currentItem.launch()
|
||||
Keys.onReturnPressed: {
|
||||
if (currentIndex >= 0) {
|
||||
GamingShell.GameLauncherProvider.launch(currentIndex)
|
||||
root.gameStarted()
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: Keys.onReturnPressed(event)
|
||||
Keys.onEscapePressed: root.dismissRequested()
|
||||
|
||||
delegate: Item {
|
||||
width: grid.cellWidth
|
||||
height: grid.cellHeight
|
||||
|
||||
GameTile {
|
||||
required property int index
|
||||
required property string name
|
||||
required property string icon
|
||||
required property string source
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
anchors.fill: parent
|
||||
folio: root.folio
|
||||
application: model.delegate ? model.delegate.application : null
|
||||
isCurrent: GridView.isCurrentItem && grid.activeFocus
|
||||
onLaunchRequested: root.gameStarted()
|
||||
|
||||
readonly property bool isCurrent: GridView.isCurrentItem && grid.activeFocus
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
color: parent.isCurrent
|
||||
? Kirigami.Theme.highlightColor
|
||||
: (parent.hovered ? Kirigami.Theme.hoverColor : "transparent")
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: Kirigami.Units.iconSizes.huge
|
||||
implicitHeight: Kirigami.Units.iconSizes.huge
|
||||
source: icon
|
||||
|
||||
scale: parent.parent.isCurrent ? 1.08 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
text: name
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
color: parent.parent.isCurrent
|
||||
? Kirigami.Theme.highlightedTextColor
|
||||
: Kirigami.Theme.textColor
|
||||
}
|
||||
|
||||
// Source badge
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: source === "steam" ? "Steam"
|
||||
: source === "flatpak" ? "Flatpak"
|
||||
: ""
|
||||
visible: source !== "desktop"
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
GamingShell.GameLauncherProvider.launch(index)
|
||||
root.gameStarted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- controller status bar ----
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: GamingShell.GamepadManager.hasGamepad
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
source: "input-gaming"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: GamingShell.GamepadManager
|
||||
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
required property string name
|
||||
required property int battery
|
||||
required property string type
|
||||
|
||||
PC3.Label {
|
||||
text: name
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||
}
|
||||
PC3.Label {
|
||||
text: battery >= 0 ? battery + "%" : ""
|
||||
visible: battery >= 0
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// Gamepad legend
|
||||
PC3.Label {
|
||||
text: i18n("A: Select B: Back Y: Exit")
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import QtQuick.Window
|
|||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
||||
import org.kde.layershell 1.0 as LayerShell
|
||||
|
||||
Window {
|
||||
|
|
@ -43,14 +44,31 @@ Window {
|
|||
radius: height / 2
|
||||
color: Qt.rgba(0, 0, 0, 0.55)
|
||||
|
||||
QQC2.ToolButton {
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
icon.name: "input-gaming"
|
||||
icon.color: "white"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: i18n("Game Center")
|
||||
onClicked: root.openRequested()
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "input-gaming"
|
||||
icon.color: "white"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: i18n("Game Center")
|
||||
onClicked: root.openRequested()
|
||||
}
|
||||
|
||||
// Show primary gamepad battery when connected
|
||||
QQC2.Label {
|
||||
visible: GamingShell.GamepadManager.hasGamepad
|
||||
&& GamingShell.GamepadManager.primaryGamepad
|
||||
&& GamingShell.GamepadManager.primaryGamepad.batteryPercent >= 0
|
||||
text: GamingShell.GamepadManager.primaryGamepad
|
||||
? GamingShell.GamepadManager.primaryGamepad.batteryPercent + "%"
|
||||
: ""
|
||||
color: "white"
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import org.kde.plasma.private.mobileshell as MobileShell
|
|||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
||||
|
||||
import org.kde.layershell 1.0 as LayerShell
|
||||
import org.kde.plasma.private.sessions 2.0
|
||||
|
|
@ -49,11 +50,24 @@ ContainmentItem {
|
|||
target: ShellSettings.Settings
|
||||
function onGamingModeEnabledChanged() {
|
||||
root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled
|
||||
GamingShell.GamepadManager.active = ShellSettings.Settings.gamingModeEnabled
|
||||
}
|
||||
}
|
||||
|
||||
// Gamepad Guide button toggles Game Center overlay
|
||||
Connections {
|
||||
target: GamingShell.GamepadManager
|
||||
enabled: ShellSettings.Settings.gamingModeEnabled
|
||||
function onButtonPressed(button, gamepadIndex) {
|
||||
if (button === GamingShell.GamepadManager.ButtonGuide) {
|
||||
root.gameCenterOpen = !root.gameCenterOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled
|
||||
GamingShell.GamepadManager.active = ShellSettings.Settings.gamingModeEnabled
|
||||
folio.FolioSettings.load();
|
||||
folio.FavouritesModel.load();
|
||||
folio.PageListModel.load();
|
||||
|
|
@ -690,12 +704,6 @@ ContainmentItem {
|
|||
gameCenterHintTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
folio.ApplicationListSearchModel.categoryFilter = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small persistent button at the top-right corner of the screen that lets
|
||||
|
|
|
|||
Loading…
Reference in a new issue