mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 15:03:09 +00:00
Merge feature/gaming-shell-mode into master
This commit is contained in:
commit
76a4a7111e
48 changed files with 6175 additions and 87 deletions
|
|
@ -112,6 +112,7 @@ set_package_properties(PlasmaWaylandProtocols PROPERTIES
|
||||||
|
|
||||||
find_package(LibKWorkspace CONFIG REQUIRED)
|
find_package(LibKWorkspace CONFIG REQUIRED)
|
||||||
find_package(Libudev REQUIRED)
|
find_package(Libudev REQUIRED)
|
||||||
|
find_package(SDL3 REQUIRED)
|
||||||
|
|
||||||
find_package(KWinDBusInterface)
|
find_package(KWinDBusInterface)
|
||||||
set_package_properties(KWinDBusInterface PROPERTIES DESCRIPTION "KWin DBus interface"
|
set_package_properties(KWinDBusInterface PROPERTIES DESCRIPTION "KWin DBus interface"
|
||||||
|
|
|
||||||
7
Nebra.code-workspace
Normal file
7
Nebra.code-workspace
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -14,3 +14,4 @@ add_subdirectory(wallpaperimageplugin)
|
||||||
add_subdirectory(dpmsplugin)
|
add_subdirectory(dpmsplugin)
|
||||||
add_subdirectory(screenbrightnessplugin)
|
add_subdirectory(screenbrightnessplugin)
|
||||||
add_subdirectory(raiselockscreenplugin)
|
add_subdirectory(raiselockscreenplugin)
|
||||||
|
add_subdirectory(gamingshellplugin)
|
||||||
|
|
|
||||||
31
components/gamingshellplugin/CMakeLists.txt
Normal file
31
components/gamingshellplugin/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
# SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
find_package(SDL3 REQUIRED)
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Sql DBus)
|
||||||
|
|
||||||
|
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
|
||||||
|
powerprofilecontrol.cpp
|
||||||
|
gamemodecontrol.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(gamingshellplugin PRIVATE
|
||||||
|
Qt::Core
|
||||||
|
Qt::DBus
|
||||||
|
Qt::Qml
|
||||||
|
Qt::Quick
|
||||||
|
Qt::Sql
|
||||||
|
KF6::I18n
|
||||||
|
KF6::KIOGui
|
||||||
|
KF6::Service
|
||||||
|
KF6::CoreAddons
|
||||||
|
KF6::ConfigCore
|
||||||
|
SDL3::SDL3
|
||||||
|
)
|
||||||
|
|
||||||
|
ecm_finalize_qml_module(gamingshellplugin)
|
||||||
1219
components/gamingshellplugin/gamelauncherprovider.cpp
Normal file
1219
components/gamingshellplugin/gamelauncherprovider.cpp
Normal file
File diff suppressed because it is too large
Load diff
146
components/gamingshellplugin/gamelauncherprovider.h
Normal file
146
components/gamingshellplugin/gamelauncherprovider.h
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QList>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QString>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <qqmlregistration.h>
|
||||||
|
|
||||||
|
#include <KConfigWatcher>
|
||||||
|
#include <KSharedConfig>
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
Q_PROPERTY(bool mangohudAvailable READ mangohudAvailable NOTIFY mangohudAvailableChanged)
|
||||||
|
Q_PROPERTY(int fpsLimit READ fpsLimit WRITE setFpsLimit NOTIFY fpsLimitChanged)
|
||||||
|
Q_PROPERTY(bool launchPending READ launchPending NOTIFY launchPendingChanged)
|
||||||
|
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
|
||||||
|
Q_PROPERTY(QString lastLaunchError READ lastLaunchError NOTIFY lastLaunchErrorChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit GameLauncherProvider(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
enum Roles {
|
||||||
|
NameRole = Qt::UserRole + 1,
|
||||||
|
IconRole,
|
||||||
|
SourceRole, // "desktop", "waydroid", "steam", "flatpak"
|
||||||
|
StorageIdRole, // .desktop file name or launch URI
|
||||||
|
LaunchCommandRole,
|
||||||
|
LaunchMethodRole,
|
||||||
|
ArtworkRole, // path to banner/grid image (empty if none)
|
||||||
|
LastPlayedTextRole,
|
||||||
|
PinnedRole,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
QString filterString() const;
|
||||||
|
void setFilterString(const QString &filter);
|
||||||
|
QString sourceFilter() const;
|
||||||
|
void setSourceFilter(const QString &source);
|
||||||
|
bool overlayEnabled() const;
|
||||||
|
void setOverlayEnabled(bool enabled);
|
||||||
|
bool mangohudAvailable() const;
|
||||||
|
int fpsLimit() const;
|
||||||
|
void setFpsLimit(int limit);
|
||||||
|
bool launchPending() const;
|
||||||
|
QString pendingLaunchName() const;
|
||||||
|
QString lastLaunchError() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void refresh();
|
||||||
|
Q_INVOKABLE void launch(int index);
|
||||||
|
Q_INVOKABLE void launchByStorageId(const QString &storageId);
|
||||||
|
Q_INVOKABLE QVariantMap gameDetails(const QString &storageId) const;
|
||||||
|
Q_INVOKABLE bool openSourceApp(const QString &source);
|
||||||
|
Q_INVOKABLE void clearLastPlayed(const QString &storageId);
|
||||||
|
Q_INVOKABLE QVariantList recentGames(int limit = 5) const;
|
||||||
|
Q_INVOKABLE void clearPendingLaunch();
|
||||||
|
Q_INVOKABLE void clearLastLaunchError();
|
||||||
|
Q_INVOKABLE void togglePin(const QString &storageId);
|
||||||
|
Q_INVOKABLE int perGameFpsLimit(const QString &storageId) const;
|
||||||
|
Q_INVOKABLE void setPerGameFpsLimit(const QString &storageId, int limit);
|
||||||
|
Q_INVOKABLE int perGameOverlayState(const QString &storageId) const;
|
||||||
|
Q_INVOKABLE void setPerGameOverlayState(const QString &storageId, int state);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void countChanged();
|
||||||
|
void loadingChanged();
|
||||||
|
void filterStringChanged();
|
||||||
|
void sourceFilterChanged();
|
||||||
|
void overlayEnabledChanged();
|
||||||
|
void mangohudAvailableChanged();
|
||||||
|
void fpsLimitChanged();
|
||||||
|
void launchPendingChanged();
|
||||||
|
void lastLaunchErrorChanged();
|
||||||
|
void gameLaunched(const QString &name);
|
||||||
|
void gameLaunchFailed(const QString &name, const QString &error);
|
||||||
|
void recentGamesChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct GameEntry {
|
||||||
|
QString name;
|
||||||
|
QString icon;
|
||||||
|
QString source;
|
||||||
|
QString storageId;
|
||||||
|
QString launchCommand;
|
||||||
|
QString artwork;
|
||||||
|
QDateTime lastPlayed;
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadDesktopGames();
|
||||||
|
void loadSteamGames();
|
||||||
|
void loadLutrisGames();
|
||||||
|
void loadHeroicGames();
|
||||||
|
void deduplicateGames();
|
||||||
|
void loadRecentTimestamps();
|
||||||
|
void saveRecentTimestamp(const QString &storageId, const QDateTime &when);
|
||||||
|
void applyFilter();
|
||||||
|
void launchEntry(GameEntry &entry);
|
||||||
|
bool launchWithMangohud(const QString &program, const QStringList &args, bool overlayEnabled, int fpsLimit, qint64 *pid = nullptr);
|
||||||
|
QString launchMethodForEntry(const GameEntry &entry) const;
|
||||||
|
QString formatLastPlayed(const QDateTime &when) const;
|
||||||
|
// Returns the current m_allGames index for the storage id.
|
||||||
|
// Callers must re-lookup after any mutation that can rebuild or reorder the list.
|
||||||
|
int findEntryIndexByStorageId(const QString &storageId) const;
|
||||||
|
void markLaunchSucceeded(const QString &storageId, const QString &name);
|
||||||
|
void markLaunchFailed(const QString &name, const QString &error);
|
||||||
|
void setPendingLaunch(const QString &name);
|
||||||
|
void loadPinnedGames();
|
||||||
|
|
||||||
|
QList<GameEntry> m_allGames;
|
||||||
|
QList<GameEntry> m_games; // filtered view
|
||||||
|
QString m_filterString;
|
||||||
|
QString m_sourceFilter; // empty = all, or a specific source such as "desktop" or "waydroid"
|
||||||
|
KSharedConfigPtr m_config;
|
||||||
|
bool m_loading = false;
|
||||||
|
bool m_overlayEnabled = false;
|
||||||
|
int m_fpsLimit = 0;
|
||||||
|
bool m_mangohudAvailable = false;
|
||||||
|
QString m_mangohudPath;
|
||||||
|
QSet<QString> m_pinnedGames;
|
||||||
|
bool m_launchPending = false;
|
||||||
|
QString m_pendingLaunchName;
|
||||||
|
QString m_lastLaunchError;
|
||||||
|
QTimer m_pendingLaunchTimer;
|
||||||
|
KConfigWatcher::Ptr m_configWatcher;
|
||||||
|
};
|
||||||
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;
|
||||||
|
};
|
||||||
240
components/gamingshellplugin/gamepaddevice.cpp
Normal file
240
components/gamingshellplugin/gamepaddevice.cpp
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
// 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>
|
||||||
|
#include <SDL3/SDL_sensor.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
static QString gamepadButtonLabelToString(SDL_GamepadButtonLabel label)
|
||||||
|
{
|
||||||
|
switch (label) {
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_A:
|
||||||
|
return QStringLiteral("A");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_B:
|
||||||
|
return QStringLiteral("B");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_X:
|
||||||
|
return QStringLiteral("X");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_Y:
|
||||||
|
return QStringLiteral("Y");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
|
||||||
|
return QStringLiteral("Cross");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
|
||||||
|
return QStringLiteral("Circle");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
|
||||||
|
return QStringLiteral("Square");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
|
||||||
|
return QStringLiteral("Triangle");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::hasTriggerRumble() const
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SDL_PropertiesID props = SDL_GetGamepadProperties(m_pad);
|
||||||
|
return SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_TRIGGER_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::touchpadCount() const
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return SDL_GetNumGamepadTouchpads(m_pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GamepadDevice::hasGyro() const
|
||||||
|
{
|
||||||
|
return m_pad && SDL_GamepadHasSensor(m_pad, SDL_SENSOR_GYRO);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GamepadDevice::hasAccelerometer() const
|
||||||
|
{
|
||||||
|
return m_pad && SDL_GamepadHasSensor(m_pad, SDL_SENSOR_ACCEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int GamepadDevice::playerIndex() const
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return SDL_GetGamepadPlayerIndex(m_pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GamepadDevice::setPlayerIndex(int index)
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SDL_SetGamepadPlayerIndex(m_pad, index)) {
|
||||||
|
Q_EMIT playerIndexChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GamepadDevice::rumble(int lowIntensity, int highIntensity, int durationMs)
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto lo = static_cast<uint16_t>(std::clamp(lowIntensity, 0, 65535));
|
||||||
|
auto hi = static_cast<uint16_t>(std::clamp(highIntensity, 0, 65535));
|
||||||
|
auto dur = static_cast<uint32_t>(std::clamp(durationMs, 0, durationMs));
|
||||||
|
return SDL_RumbleGamepad(m_pad, lo, hi, dur);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GamepadDevice::rumbleTriggers(int leftIntensity, int rightIntensity, int durationMs)
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto left = static_cast<uint16_t>(std::clamp(leftIntensity, 0, 65535));
|
||||||
|
auto right = static_cast<uint16_t>(std::clamp(rightIntensity, 0, 65535));
|
||||||
|
auto dur = static_cast<uint32_t>(std::clamp(durationMs, 0, durationMs));
|
||||||
|
return SDL_RumbleGamepadTriggers(m_pad, left, right, dur);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GamepadDevice::setLED(int r, int g, int b)
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto cr = static_cast<uint8_t>(std::clamp(r, 0, 255));
|
||||||
|
auto cg = static_cast<uint8_t>(std::clamp(g, 0, 255));
|
||||||
|
auto cb = static_cast<uint8_t>(std::clamp(b, 0, 255));
|
||||||
|
return SDL_SetGamepadLED(m_pad, cr, cg, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GamepadDevice::buttonLabel(int button) const
|
||||||
|
{
|
||||||
|
if (!m_pad) {
|
||||||
|
return QStringLiteral("?");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (button) {
|
||||||
|
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||||
|
case SDL_GAMEPAD_BUTTON_EAST:
|
||||||
|
case SDL_GAMEPAD_BUTTON_WEST:
|
||||||
|
case SDL_GAMEPAD_BUTTON_NORTH:
|
||||||
|
return gamepadButtonLabelToString(SDL_GetGamepadButtonLabel(m_pad, static_cast<SDL_GamepadButton>(button)));
|
||||||
|
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||||
|
return type() == QLatin1String("playstation") ? QStringLiteral("L1") : type() == QLatin1String("nintendo") ? QStringLiteral("L") : QStringLiteral("LB");
|
||||||
|
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||||
|
return type() == QLatin1String("playstation") ? QStringLiteral("R1") : type() == QLatin1String("nintendo") ? QStringLiteral("R") : QStringLiteral("RB");
|
||||||
|
case SDL_GAMEPAD_BUTTON_BACK:
|
||||||
|
return type() == QLatin1String("playstation") ? QStringLiteral("Create")
|
||||||
|
: type() == QLatin1String("nintendo") ? QStringLiteral("-")
|
||||||
|
: QStringLiteral("View");
|
||||||
|
case SDL_GAMEPAD_BUTTON_START:
|
||||||
|
return type() == QLatin1String("playstation") ? QStringLiteral("Options")
|
||||||
|
: type() == QLatin1String("nintendo") ? QStringLiteral("+")
|
||||||
|
: QStringLiteral("Menu");
|
||||||
|
case SDL_GAMEPAD_BUTTON_GUIDE:
|
||||||
|
return type() == QLatin1String("playstation") ? QStringLiteral("PS")
|
||||||
|
: type() == QLatin1String("nintendo") ? QStringLiteral("Home")
|
||||||
|
: QStringLiteral("Guide");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
63
components/gamingshellplugin/gamepaddevice.h
Normal file
63
components/gamingshellplugin/gamepaddevice.h
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// 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 hasTriggerRumble READ hasTriggerRumble CONSTANT)
|
||||||
|
Q_PROPERTY(bool hasLED READ hasLED CONSTANT)
|
||||||
|
Q_PROPERTY(int touchpadCount READ touchpadCount CONSTANT)
|
||||||
|
Q_PROPERTY(bool hasGyro READ hasGyro CONSTANT)
|
||||||
|
Q_PROPERTY(bool hasAccelerometer READ hasAccelerometer 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 hasTriggerRumble() const;
|
||||||
|
bool hasLED() const;
|
||||||
|
int touchpadCount() const;
|
||||||
|
bool hasGyro() const;
|
||||||
|
bool hasAccelerometer() const;
|
||||||
|
int playerIndex() const;
|
||||||
|
void setPlayerIndex(int index);
|
||||||
|
|
||||||
|
Q_INVOKABLE bool rumble(int lowIntensity, int highIntensity, int durationMs);
|
||||||
|
Q_INVOKABLE bool rumbleTriggers(int leftIntensity, int rightIntensity, int durationMs);
|
||||||
|
Q_INVOKABLE bool setLED(int r, int g, int b);
|
||||||
|
Q_INVOKABLE QString buttonLabel(int button) const;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
314
components/gamingshellplugin/gamepadmanager.cpp
Normal file
314
components/gamingshellplugin/gamepadmanager.cpp
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
// 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 <SDL3/SDL_hints.h>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
GamepadManager::GamepadManager(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
m_pollTimer.setInterval(16); // ~60 Hz
|
||||||
|
connect(&m_pollTimer, &QTimer::timeout, this, &GamepadManager::poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
GamepadManager::~GamepadManager()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
GamepadManager *GamepadManager::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
|
||||||
|
{
|
||||||
|
Q_UNUSED(jsEngine)
|
||||||
|
static GamepadManager *s_instance = nullptr;
|
||||||
|
if (!s_instance) {
|
||||||
|
s_instance = new GamepadManager(qmlEngine);
|
||||||
|
}
|
||||||
|
return s_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GamepadManager::buttonLabel(int button, int gamepadIndex) const
|
||||||
|
{
|
||||||
|
GamepadDevice *device = nullptr;
|
||||||
|
if (gamepadIndex >= 0 && gamepadIndex < m_gamepads.size()) {
|
||||||
|
device = m_gamepads.at(gamepadIndex);
|
||||||
|
} else {
|
||||||
|
device = primaryGamepad();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
switch (button) {
|
||||||
|
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||||
|
return QStringLiteral("A");
|
||||||
|
case SDL_GAMEPAD_BUTTON_EAST:
|
||||||
|
return QStringLiteral("B");
|
||||||
|
case SDL_GAMEPAD_BUTTON_WEST:
|
||||||
|
return QStringLiteral("X");
|
||||||
|
case SDL_GAMEPAD_BUTTON_NORTH:
|
||||||
|
return QStringLiteral("Y");
|
||||||
|
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||||
|
return QStringLiteral("LB");
|
||||||
|
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||||
|
return QStringLiteral("RB");
|
||||||
|
case SDL_GAMEPAD_BUTTON_BACK:
|
||||||
|
return QStringLiteral("Back");
|
||||||
|
case SDL_GAMEPAD_BUTTON_START:
|
||||||
|
return QStringLiteral("Start");
|
||||||
|
case SDL_GAMEPAD_BUTTON_GUIDE:
|
||||||
|
return QStringLiteral("Guide");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return device->buttonLabel(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lifecycle ---
|
||||||
|
|
||||||
|
void GamepadManager::start()
|
||||||
|
{
|
||||||
|
if (m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
|
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
|
||||||
|
qWarning() << "GamepadManager: SDL_Init failed:" << SDL_GetError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_sdlInitialized = true;
|
||||||
|
SDL_SetGamepadEventsEnabled(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 = std::clamp(static_cast<float>(event.gaxis.value) / 32767.0f, -1.0f, 1.0f);
|
||||||
|
Q_EMIT axisChanged(event.gaxis.axis, normalized, idx);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh battery state periodically (every ~5 seconds = 300 frames)
|
||||||
|
if (++m_batteryCounter >= 300) {
|
||||||
|
m_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;
|
||||||
|
}
|
||||||
106
components/gamingshellplugin/gamepadmanager.h
Normal file
106
components/gamingshellplugin/gamepadmanager.h
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <qqmlregistration.h>
|
||||||
|
|
||||||
|
class QQmlEngine;
|
||||||
|
class QJSEngine;
|
||||||
|
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;
|
||||||
|
|
||||||
|
static GamepadManager *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Axes matching SDL_GamepadAxis
|
||||||
|
enum Axis {
|
||||||
|
AxisLeftX,
|
||||||
|
AxisLeftY,
|
||||||
|
AxisRightX,
|
||||||
|
AxisRightY,
|
||||||
|
AxisLeftTrigger,
|
||||||
|
AxisRightTrigger,
|
||||||
|
};
|
||||||
|
Q_ENUM(Axis)
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
GamepadDevice *gamepadAt(int index) const;
|
||||||
|
Q_INVOKABLE QString buttonLabel(int button, int gamepadIndex = -1) 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;
|
||||||
|
int m_batteryCounter = 0;
|
||||||
|
QTimer m_pollTimer;
|
||||||
|
QList<GamepadDevice *> m_gamepads;
|
||||||
|
};
|
||||||
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;
|
||||||
|
};
|
||||||
|
|
@ -86,13 +86,15 @@ QQuickItem *AppletHost::fullRepresentationFor(const QString &pluginId)
|
||||||
|
|
||||||
m_containment->addApplet(applet);
|
m_containment->addApplet(applet);
|
||||||
auto *item = PlasmaQuick::AppletQuickItem::itemForApplet(applet);
|
auto *item = PlasmaQuick::AppletQuickItem::itemForApplet(applet);
|
||||||
m_items.insert(pluginId, item);
|
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
qWarning() << "AppletHost: no AppletQuickItem for" << pluginId;
|
qWarning() << "AppletHost: no AppletQuickItem for" << pluginId;
|
||||||
|
applet->deleteLater();
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_items.insert(pluginId, item);
|
||||||
|
|
||||||
item->setPreloadFullRepresentation(true);
|
item->setPreloadFullRepresentation(true);
|
||||||
|
|
||||||
auto *fullRepItem = item->fullRepresentationItem();
|
auto *fullRepItem = item->fullRepresentationItem();
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ Item {
|
||||||
shrinkSize: 0
|
shrinkSize: 0
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
if (!root.taskModel) return;
|
||||||
root.taskModel.requestActivate(root.taskModel.makeModelIndex(taskDelegate.index));
|
root.taskModel.requestActivate(root.taskModel.makeModelIndex(taskDelegate.index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,18 +181,27 @@ Item {
|
||||||
Controls.MenuItem {
|
Controls.MenuItem {
|
||||||
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
|
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
|
||||||
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"
|
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"
|
||||||
onTriggered: root.taskModel.requestToggleMinimized(root.taskModel.makeModelIndex(taskDelegate.index))
|
onTriggered: {
|
||||||
|
if (!root.taskModel) return;
|
||||||
|
root.taskModel.requestToggleMinimized(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Controls.MenuItem {
|
Controls.MenuItem {
|
||||||
text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize")
|
text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize")
|
||||||
icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize"
|
icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize"
|
||||||
onTriggered: root.taskModel.requestToggleMaximized(root.taskModel.makeModelIndex(taskDelegate.index))
|
onTriggered: {
|
||||||
|
if (!root.taskModel) return;
|
||||||
|
root.taskModel.requestToggleMaximized(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Controls.MenuSeparator {}
|
Controls.MenuSeparator {}
|
||||||
Controls.MenuItem {
|
Controls.MenuItem {
|
||||||
text: i18n("Close")
|
text: i18n("Close")
|
||||||
icon.name: "window-close"
|
icon.name: "window-close"
|
||||||
onTriggered: root.taskModel.requestClose(root.taskModel.makeModelIndex(taskDelegate.index))
|
onTriggered: {
|
||||||
|
if (!root.taskModel) return;
|
||||||
|
root.taskModel.requestClose(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ QList<QString> QuickSettingsConfig::enabledQuickSettings() const
|
||||||
QStringLiteral("org.kde.plasma.quicksetting.screenshot"),
|
QStringLiteral("org.kde.plasma.quicksetting.screenshot"),
|
||||||
QStringLiteral("org.kde.plasma.quicksetting.powermenu"),
|
QStringLiteral("org.kde.plasma.quicksetting.powermenu"),
|
||||||
QStringLiteral("org.kde.plasma.quicksetting.donotdisturb"),
|
QStringLiteral("org.kde.plasma.quicksetting.donotdisturb"),
|
||||||
|
QStringLiteral("org.kde.plasma.quicksetting.gaming"),
|
||||||
|
QStringLiteral("org.kde.plasma.quicksetting.gaminghint"),
|
||||||
QStringLiteral("org.kde.plasma.quicksetting.caffeine"),
|
QStringLiteral("org.kde.plasma.quicksetting.caffeine"),
|
||||||
QStringLiteral("org.kde.plasma.quicksetting.keyboardtoggle"),
|
QStringLiteral("org.kde.plasma.quicksetting.keyboardtoggle"),
|
||||||
QStringLiteral("org.kde.plasma.quicksetting.hotspot")});
|
QStringLiteral("org.kde.plasma.quicksetting.hotspot")});
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
|
||||||
Q_EMIT actionDrawerTopRightModeChanged();
|
Q_EMIT actionDrawerTopRightModeChanged();
|
||||||
Q_EMIT convergenceModeEnabledChanged();
|
Q_EMIT convergenceModeEnabledChanged();
|
||||||
Q_EMIT autoHidePanelsEnabledChanged();
|
Q_EMIT autoHidePanelsEnabledChanged();
|
||||||
|
Q_EMIT gamingModeEnabledChanged();
|
||||||
|
Q_EMIT gamingDismissHintEnabledChanged();
|
||||||
Q_EMIT allowLogoutChanged();
|
Q_EMIT allowLogoutChanged();
|
||||||
}
|
}
|
||||||
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
|
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
|
||||||
|
|
@ -248,6 +250,32 @@ void MobileShellSettings::setAutoHidePanelsEnabled(bool enabled)
|
||||||
m_config->sync();
|
m_config->sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MobileShellSettings::gamingModeEnabled() const
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||||
|
return group.readEntry("gamingModeEnabled", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MobileShellSettings::setGamingModeEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||||
|
group.writeEntry("gamingModeEnabled", enabled, KConfigGroup::Notify);
|
||||||
|
m_config->sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MobileShellSettings::gamingDismissHintEnabled() const
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||||
|
return group.readEntry("gamingDismissHintEnabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MobileShellSettings::setGamingDismissHintEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||||
|
group.writeEntry("gamingDismissHintEnabled", enabled, KConfigGroup::Notify);
|
||||||
|
m_config->sync();
|
||||||
|
}
|
||||||
|
|
||||||
void MobileShellSettings::updateNavigationBarsInPlasma()
|
void MobileShellSettings::updateNavigationBarsInPlasma()
|
||||||
{
|
{
|
||||||
// Do not update panels when not in Plasma Mobile
|
// Do not update panels when not in Plasma Mobile
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ class MobileShellSettings : public QObject
|
||||||
// Auto Hide Panels
|
// Auto Hide Panels
|
||||||
Q_PROPERTY(bool autoHidePanelsEnabled READ autoHidePanelsEnabled WRITE setAutoHidePanelsEnabled NOTIFY autoHidePanelsEnabledChanged)
|
Q_PROPERTY(bool autoHidePanelsEnabled READ autoHidePanelsEnabled WRITE setAutoHidePanelsEnabled NOTIFY autoHidePanelsEnabledChanged)
|
||||||
|
|
||||||
|
// Gaming mode
|
||||||
|
Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY gamingModeEnabledChanged)
|
||||||
|
Q_PROPERTY(bool gamingDismissHintEnabled READ gamingDismissHintEnabled WRITE setGamingDismissHintEnabled NOTIFY gamingDismissHintEnabledChanged)
|
||||||
|
|
||||||
// logout dialog
|
// logout dialog
|
||||||
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
|
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
|
||||||
|
|
||||||
|
|
@ -264,6 +268,12 @@ public:
|
||||||
*/
|
*/
|
||||||
void setAutoHidePanelsEnabled(bool enabled);
|
void setAutoHidePanelsEnabled(bool enabled);
|
||||||
|
|
||||||
|
bool gamingModeEnabled() const;
|
||||||
|
void setGamingModeEnabled(bool enabled);
|
||||||
|
|
||||||
|
bool gamingDismissHintEnabled() const;
|
||||||
|
void setGamingDismissHintEnabled(bool enabled);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether logout button is shown in the logout/shutdown dialog.
|
* Whether logout button is shown in the logout/shutdown dialog.
|
||||||
*/
|
*/
|
||||||
|
|
@ -310,6 +320,8 @@ Q_SIGNALS:
|
||||||
void quickSettingsColumnsChanged();
|
void quickSettingsColumnsChanged();
|
||||||
void convergenceModeEnabledChanged();
|
void convergenceModeEnabledChanged();
|
||||||
void autoHidePanelsEnabledChanged();
|
void autoHidePanelsEnabledChanged();
|
||||||
|
void gamingModeEnabledChanged();
|
||||||
|
void gamingDismissHintEnabledChanged();
|
||||||
void allowLogoutChanged();
|
void allowLogoutChanged();
|
||||||
void lockscreenLeftButtonActionChanged();
|
void lockscreenLeftButtonActionChanged();
|
||||||
void lockscreenRightButtonActionChanged();
|
void lockscreenRightButtonActionChanged();
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,17 @@ void WaydroidApplicationListModel::initializeApplications(const QList<QDBusObjec
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WaydroidApplicationListModel::clearApplications()
|
||||||
|
{
|
||||||
|
if (m_applications.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginResetModel();
|
||||||
|
m_applications.clear();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
void WaydroidApplicationListModel::addApplication(const QDBusObjectPath &objectPath)
|
void WaydroidApplicationListModel::addApplication(const QDBusObjectPath &objectPath)
|
||||||
{
|
{
|
||||||
beginInsertRows({}, m_applications.size(), m_applications.size());
|
beginInsertRows({}, m_applications.size(), m_applications.size());
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ public:
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
void initializeApplications(const QList<QDBusObjectPath> &applicationObjectPaths);
|
void initializeApplications(const QList<QDBusObjectPath> &applicationObjectPaths);
|
||||||
|
void clearApplications();
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void addApplication(const QDBusObjectPath &objectPath);
|
void addApplication(const QDBusObjectPath &objectPath);
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,38 @@
|
||||||
|
|
||||||
#include "waydroiddbusclient.h"
|
#include "waydroiddbusclient.h"
|
||||||
|
|
||||||
|
#include <KConfigGroup>
|
||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QCoroDBusPendingReply>
|
#include <QCoroDBusPendingReply>
|
||||||
|
#include <QDBusMessage>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
static const QString s_waydroidGamingGroup = QStringLiteral("WaydroidGaming");
|
||||||
|
static const QString s_gameShellPackagesKey = QStringLiteral("gameShellPackages");
|
||||||
|
|
||||||
WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
|
WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
|
||||||
: QObject{parent}
|
: QObject{parent}
|
||||||
, m_interface{new OrgKdePlasmashellWaydroidInterface{u"org.kde.plasmashell"_s, u"/Waydroid"_s, QDBusConnection::sessionBus(), this}}
|
, m_interface{new OrgKdePlasmashellWaydroidInterface{u"org.kde.plasmashell"_s, u"/Waydroid"_s, QDBusConnection::sessionBus(), this}}
|
||||||
, m_watcher{new QDBusServiceWatcher{u"org.kde.plasmashell"_s, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this}}
|
, m_watcher{new QDBusServiceWatcher{u"org.kde.plasmashell"_s, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this}}
|
||||||
, m_applicationListModel{new WaydroidApplicationListModel{this}}
|
, m_applicationListModel{new WaydroidApplicationListModel{this}}
|
||||||
|
, m_config{KSharedConfig::openConfig(QStringLiteral("plasmamobilerc"))}
|
||||||
{
|
{
|
||||||
|
m_configWatcher = KConfigWatcher::create(m_config);
|
||||||
|
connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group) {
|
||||||
|
if (group.name() == s_waydroidGamingGroup) {
|
||||||
|
m_config->reparseConfiguration();
|
||||||
|
reloadGameShellPackages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reloadGameShellPackages();
|
||||||
|
|
||||||
// Check if the service is already running
|
// Check if the service is already running
|
||||||
if (QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.plasmashell"_s)) {
|
if (QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.plasmashell"_s)) {
|
||||||
m_connected = true;
|
checkWaydroidObject();
|
||||||
if (m_interface->isValid()) {
|
|
||||||
connectSignals();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this](const QString &service, const QString &oldOwner, const QString &newOwner) {
|
connect(m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this](const QString &service, const QString &oldOwner, const QString &newOwner) {
|
||||||
|
|
@ -31,12 +45,11 @@ WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
|
||||||
if (newOwner.isEmpty()) {
|
if (newOwner.isEmpty()) {
|
||||||
// Service stopped
|
// Service stopped
|
||||||
m_connected = false;
|
m_connected = false;
|
||||||
|
m_connectionCheckPending = false;
|
||||||
|
resetState();
|
||||||
} else if (oldOwner.isEmpty()) {
|
} else if (oldOwner.isEmpty()) {
|
||||||
// Service started
|
// Service started
|
||||||
m_connected = true;
|
checkWaydroidObject();
|
||||||
if (m_interface->isValid()) {
|
|
||||||
connectSignals();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -44,6 +57,9 @@ WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
|
||||||
|
|
||||||
void WaydroidDBusClient::connectSignals()
|
void WaydroidDBusClient::connectSignals()
|
||||||
{
|
{
|
||||||
|
if (!m_signalsConnected) {
|
||||||
|
m_signalsConnected = true;
|
||||||
|
|
||||||
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::statusChanged, this, &WaydroidDBusClient::updateStatus);
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::statusChanged, this, &WaydroidDBusClient::updateStatus);
|
||||||
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::downloadStatusChanged, this, [this](double downloaded, double total, double speed) {
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::downloadStatusChanged, this, [this](double downloaded, double total, double speed) {
|
||||||
Q_EMIT downloadStatusChanged(downloaded, total, speed);
|
Q_EMIT downloadStatusChanged(downloaded, total, speed);
|
||||||
|
|
@ -55,6 +71,8 @@ void WaydroidDBusClient::connectSignals()
|
||||||
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::multiWindowsChanged, this, &WaydroidDBusClient::updateMultiWindows);
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::multiWindowsChanged, this, &WaydroidDBusClient::updateMultiWindows);
|
||||||
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::suspendChanged, this, &WaydroidDBusClient::updateSuspend);
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::suspendChanged, this, &WaydroidDBusClient::updateSuspend);
|
||||||
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::ueventChanged, this, &WaydroidDBusClient::updateUevent);
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::ueventChanged, this, &WaydroidDBusClient::updateUevent);
|
||||||
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::fakeTouchChanged, this, &WaydroidDBusClient::updateFakeTouch);
|
||||||
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::fakeWifiChanged, this, &WaydroidDBusClient::updateFakeWifi);
|
||||||
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::actionFinished, this, [this](const QString message) {
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::actionFinished, this, [this](const QString message) {
|
||||||
Q_EMIT actionFinished(message);
|
Q_EMIT actionFinished(message);
|
||||||
});
|
});
|
||||||
|
|
@ -64,6 +82,7 @@ void WaydroidDBusClient::connectSignals()
|
||||||
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::errorOccurred, this, [this](const QString title, const QString message) {
|
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::errorOccurred, this, [this](const QString title, const QString message) {
|
||||||
Q_EMIT errorOccurred(title, message);
|
Q_EMIT errorOccurred(title, message);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initializeApplicationListModel();
|
initializeApplicationListModel();
|
||||||
updateStatus();
|
updateStatus();
|
||||||
|
|
@ -74,6 +93,118 @@ void WaydroidDBusClient::connectSignals()
|
||||||
updateMultiWindows();
|
updateMultiWindows();
|
||||||
updateSuspend();
|
updateSuspend();
|
||||||
updateUevent();
|
updateUevent();
|
||||||
|
updateFakeTouch();
|
||||||
|
updateFakeWifi();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::checkWaydroidObject()
|
||||||
|
{
|
||||||
|
if (m_connectionCheckPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_connectionCheckPending = true;
|
||||||
|
|
||||||
|
const QDBusMessage message =
|
||||||
|
QDBusMessage::createMethodCall(u"org.kde.plasmashell"_s, u"/Waydroid"_s, u"org.freedesktop.DBus.Introspectable"_s, u"Introspect"_s);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, &WaydroidDBusClient::onWaydroidObjectCheckFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::onWaydroidObjectCheckFinished(QDBusPendingCallWatcher *watcher)
|
||||||
|
{
|
||||||
|
m_connectionCheckPending = false;
|
||||||
|
|
||||||
|
QDBusPendingReply<QString> reply = *watcher;
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
m_connected = false;
|
||||||
|
watcher->deleteLater();
|
||||||
|
resetState();
|
||||||
|
scheduleWaydroidObjectCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_connected = true;
|
||||||
|
watcher->deleteLater();
|
||||||
|
|
||||||
|
if (m_interface->isValid()) {
|
||||||
|
connectSignals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::handleUnavailableReply()
|
||||||
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_connected = false;
|
||||||
|
resetState();
|
||||||
|
scheduleWaydroidObjectCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::resetState()
|
||||||
|
{
|
||||||
|
if (m_status != NotSupported) {
|
||||||
|
m_status = NotSupported;
|
||||||
|
Q_EMIT statusChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_sessionStatus != SessionStopped) {
|
||||||
|
m_sessionStatus = SessionStopped;
|
||||||
|
Q_EMIT sessionStatusChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_systemType != UnknownSystemType) {
|
||||||
|
m_systemType = UnknownSystemType;
|
||||||
|
Q_EMIT systemTypeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_ipAddress.isEmpty()) {
|
||||||
|
m_ipAddress.clear();
|
||||||
|
Q_EMIT ipAddressChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_androidId.isEmpty()) {
|
||||||
|
m_androidId.clear();
|
||||||
|
Q_EMIT androidIdChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_multiWindows) {
|
||||||
|
m_multiWindows = false;
|
||||||
|
Q_EMIT multiWindowsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_suspend) {
|
||||||
|
m_suspend = false;
|
||||||
|
Q_EMIT suspendChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_uevent) {
|
||||||
|
m_uevent = false;
|
||||||
|
Q_EMIT ueventChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_fakeTouch.isEmpty()) {
|
||||||
|
m_fakeTouch.clear();
|
||||||
|
Q_EMIT fakeTouchChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_fakeWifi.isEmpty()) {
|
||||||
|
m_fakeWifi.clear();
|
||||||
|
Q_EMIT fakeWifiChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_applicationListModel->clearApplications();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::scheduleWaydroidObjectCheck()
|
||||||
|
{
|
||||||
|
if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.plasmashell"_s)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTimer::singleShot(1000, this, &WaydroidDBusClient::checkWaydroidObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WaydroidDBusClient::initializeApplicationListModel()
|
void WaydroidDBusClient::initializeApplicationListModel()
|
||||||
|
|
@ -85,6 +216,7 @@ void WaydroidDBusClient::initializeApplicationListModel()
|
||||||
QDBusPendingReply<QList<QDBusObjectPath>> reply = *watcher;
|
QDBusPendingReply<QList<QDBusObjectPath>> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch applications:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch applications:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +265,10 @@ WaydroidApplicationListModel *WaydroidDBusClient::applicationListModel() const
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::setMultiWindowsTask(const bool multiWindows)
|
QCoro::Task<void> WaydroidDBusClient::setMultiWindowsTask(const bool multiWindows)
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->setMultiWindows(multiWindows);
|
co_await m_interface->setMultiWindows(multiWindows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,6 +284,10 @@ bool WaydroidDBusClient::multiWindows() const
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::setSuspendTask(const bool suspend)
|
QCoro::Task<void> WaydroidDBusClient::setSuspendTask(const bool suspend)
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->setSuspend(suspend);
|
co_await m_interface->setSuspend(suspend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,6 +303,10 @@ bool WaydroidDBusClient::suspend() const
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::setUeventTask(const bool uevent)
|
QCoro::Task<void> WaydroidDBusClient::setUeventTask(const bool uevent)
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->setUevent(uevent);
|
co_await m_interface->setUevent(uevent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,6 +317,10 @@ QCoro::QmlTask WaydroidDBusClient::setUevent(const bool uevent)
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::refreshSessionInfoTask()
|
QCoro::Task<void> WaydroidDBusClient::refreshSessionInfoTask()
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->refreshSessionInfo();
|
co_await m_interface->refreshSessionInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +331,10 @@ QCoro::QmlTask WaydroidDBusClient::refreshSessionInfo()
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::refreshAndroidIdTask()
|
QCoro::Task<void> WaydroidDBusClient::refreshAndroidIdTask()
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->refreshAndroidId();
|
co_await m_interface->refreshAndroidId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,6 +345,10 @@ QCoro::QmlTask WaydroidDBusClient::refreshAndroidId()
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::refreshApplicationsTask()
|
QCoro::Task<void> WaydroidDBusClient::refreshApplicationsTask()
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->refreshApplications();
|
co_await m_interface->refreshApplications();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,13 +357,87 @@ QCoro::QmlTask WaydroidDBusClient::refreshApplications()
|
||||||
return refreshApplicationsTask();
|
return refreshApplicationsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WaydroidDBusClient::gameShellEnabledForPackage(const QString &packageName) const
|
||||||
|
{
|
||||||
|
return m_gameShellPackages.contains(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::setGameShellEnabledForPackage(const QString &packageName, bool enabled)
|
||||||
|
{
|
||||||
|
QStringList packages = m_gameShellPackages;
|
||||||
|
packages.removeAll(packageName);
|
||||||
|
if (enabled) {
|
||||||
|
packages.append(packageName);
|
||||||
|
}
|
||||||
|
packages.removeDuplicates();
|
||||||
|
packages.sort();
|
||||||
|
|
||||||
|
if (packages == m_gameShellPackages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KConfigGroup group(m_config, s_waydroidGamingGroup);
|
||||||
|
group.writeEntry(s_gameShellPackagesKey, packages, KConfigGroup::Notify);
|
||||||
|
m_config->sync();
|
||||||
|
|
||||||
|
m_gameShellPackages = packages;
|
||||||
|
Q_EMIT gameShellPackagesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
bool WaydroidDBusClient::uevent() const
|
bool WaydroidDBusClient::uevent() const
|
||||||
{
|
{
|
||||||
return m_uevent;
|
return m_uevent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> WaydroidDBusClient::setFakeTouchTask(const QString &fakeTouch)
|
||||||
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await m_interface->setFakeTouch(fakeTouch);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::QmlTask WaydroidDBusClient::setFakeTouch(const QString &fakeTouch)
|
||||||
|
{
|
||||||
|
return setFakeTouchTask(fakeTouch);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WaydroidDBusClient::fakeTouch() const
|
||||||
|
{
|
||||||
|
return m_fakeTouch;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> WaydroidDBusClient::setFakeWifiTask(const QString &fakeWifi)
|
||||||
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await m_interface->setFakeWifi(fakeWifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::QmlTask WaydroidDBusClient::setFakeWifi(const QString &fakeWifi)
|
||||||
|
{
|
||||||
|
return setFakeWifiTask(fakeWifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WaydroidDBusClient::fakeWifi() const
|
||||||
|
{
|
||||||
|
return m_fakeWifi;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList WaydroidDBusClient::gameShellPackages() const
|
||||||
|
{
|
||||||
|
return m_gameShellPackages;
|
||||||
|
}
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::initializeTask(const SystemType systemType, const RomType romType, const bool forced)
|
QCoro::Task<void> WaydroidDBusClient::initializeTask(const SystemType systemType, const RomType romType, const bool forced)
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->initialize(systemType, romType, forced);
|
co_await m_interface->initialize(systemType, romType, forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,6 +448,10 @@ QCoro::QmlTask WaydroidDBusClient::initialize(const SystemType systemType, const
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::startSessionTask()
|
QCoro::Task<void> WaydroidDBusClient::startSessionTask()
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->startSession();
|
co_await m_interface->startSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,6 +462,10 @@ QCoro::QmlTask WaydroidDBusClient::startSession()
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::stopSessionTask()
|
QCoro::Task<void> WaydroidDBusClient::stopSessionTask()
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->stopSession();
|
co_await m_interface->stopSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,6 +476,10 @@ QCoro::QmlTask WaydroidDBusClient::stopSession()
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::resetWaydroidTask()
|
QCoro::Task<void> WaydroidDBusClient::resetWaydroidTask()
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->resetWaydroid();
|
co_await m_interface->resetWaydroid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,6 +490,10 @@ QCoro::QmlTask WaydroidDBusClient::resetWaydroid()
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::installApkTask(const QString apkFile)
|
QCoro::Task<void> WaydroidDBusClient::installApkTask(const QString apkFile)
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->installApk(apkFile);
|
co_await m_interface->installApk(apkFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,8 +502,26 @@ QCoro::QmlTask WaydroidDBusClient::installApk(const QString apkFile)
|
||||||
return installApkTask(apkFile);
|
return installApkTask(apkFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> WaydroidDBusClient::launchApplicationTask(const QString appId)
|
||||||
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await m_interface->launchApplication(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::QmlTask WaydroidDBusClient::launchApplication(const QString appId)
|
||||||
|
{
|
||||||
|
return launchApplicationTask(appId);
|
||||||
|
}
|
||||||
|
|
||||||
QCoro::Task<void> WaydroidDBusClient::deleteApplicationTask(const QString appId)
|
QCoro::Task<void> WaydroidDBusClient::deleteApplicationTask(const QString appId)
|
||||||
{
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await m_interface->deleteApplication(appId);
|
co_await m_interface->deleteApplication(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,6 +530,20 @@ QCoro::QmlTask WaydroidDBusClient::deleteApplication(const QString appId)
|
||||||
return deleteApplicationTask(appId);
|
return deleteApplicationTask(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> WaydroidDBusClient::refreshSupportsInfoTask()
|
||||||
|
{
|
||||||
|
if (!m_connected) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await m_interface->refreshSupportsInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::QmlTask WaydroidDBusClient::refreshSupportsInfo()
|
||||||
|
{
|
||||||
|
return refreshSupportsInfoTask();
|
||||||
|
}
|
||||||
|
|
||||||
void WaydroidDBusClient::updateStatus()
|
void WaydroidDBusClient::updateStatus()
|
||||||
{
|
{
|
||||||
auto reply = m_interface->status();
|
auto reply = m_interface->status();
|
||||||
|
|
@ -275,6 +553,7 @@ void WaydroidDBusClient::updateStatus()
|
||||||
QDBusPendingReply<int> reply = *watcher;
|
QDBusPendingReply<int> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch status:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch status:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -299,6 +578,7 @@ void WaydroidDBusClient::updateSessionStatus()
|
||||||
QDBusPendingReply<int> reply = *watcher;
|
QDBusPendingReply<int> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch sessionStatus:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch sessionStatus:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -323,6 +603,7 @@ void WaydroidDBusClient::updateSystemType()
|
||||||
QDBusPendingReply<int> reply = *watcher;
|
QDBusPendingReply<int> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch systemType:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch systemType:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -347,6 +628,7 @@ void WaydroidDBusClient::updateIpAddress()
|
||||||
QDBusPendingReply<QString> reply = *watcher;
|
QDBusPendingReply<QString> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch ipAddress:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch ipAddress:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +653,7 @@ void WaydroidDBusClient::updateAndroidId()
|
||||||
QDBusPendingReply<QString> reply = *watcher;
|
QDBusPendingReply<QString> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch androidId:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch androidId:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -395,6 +678,7 @@ void WaydroidDBusClient::updateMultiWindows()
|
||||||
QDBusPendingReply<bool> reply = *watcher;
|
QDBusPendingReply<bool> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch multiWindows:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch multiWindows:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -419,6 +703,7 @@ void WaydroidDBusClient::updateSuspend()
|
||||||
QDBusPendingReply<bool> reply = *watcher;
|
QDBusPendingReply<bool> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch suspend:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch suspend:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -443,6 +728,7 @@ void WaydroidDBusClient::updateUevent()
|
||||||
QDBusPendingReply<bool> reply = *watcher;
|
QDBusPendingReply<bool> reply = *watcher;
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
qDebug() << "WaydroidDBusClient: Failed to fetch uevent:" << reply.error().message();
|
qDebug() << "WaydroidDBusClient: Failed to fetch uevent:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -458,7 +744,72 @@ void WaydroidDBusClient::updateUevent()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::updateFakeTouch()
|
||||||
|
{
|
||||||
|
auto reply = m_interface->fakeTouch();
|
||||||
|
auto watcher = new QDBusPendingCallWatcher(reply, this);
|
||||||
|
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
|
||||||
|
QDBusPendingReply<QString> reply = *watcher;
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
qDebug() << "WaydroidDBusClient: Failed to fetch fakeTouch:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString fakeTouch = reply.argumentAt<0>();
|
||||||
|
|
||||||
|
if (m_fakeTouch != fakeTouch) {
|
||||||
|
m_fakeTouch = fakeTouch;
|
||||||
|
Q_EMIT fakeTouchChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::updateFakeWifi()
|
||||||
|
{
|
||||||
|
auto reply = m_interface->fakeWifi();
|
||||||
|
auto watcher = new QDBusPendingCallWatcher(reply, this);
|
||||||
|
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
|
||||||
|
QDBusPendingReply<QString> reply = *watcher;
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
qDebug() << "WaydroidDBusClient: Failed to fetch fakeWifi:" << reply.error().message();
|
||||||
|
handleUnavailableReply();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString fakeWifi = reply.argumentAt<0>();
|
||||||
|
|
||||||
|
if (m_fakeWifi != fakeWifi) {
|
||||||
|
m_fakeWifi = fakeWifi;
|
||||||
|
Q_EMIT fakeWifiChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void WaydroidDBusClient::copyToClipboard(const QString text)
|
void WaydroidDBusClient::copyToClipboard(const QString text)
|
||||||
{
|
{
|
||||||
qGuiApp->clipboard()->setText(text);
|
qGuiApp->clipboard()->setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusClient::reloadGameShellPackages()
|
||||||
|
{
|
||||||
|
const KConfigGroup group(m_config, s_waydroidGamingGroup);
|
||||||
|
QStringList packages = group.readEntry(s_gameShellPackagesKey, QStringList{});
|
||||||
|
packages.removeDuplicates();
|
||||||
|
packages.sort();
|
||||||
|
|
||||||
|
if (m_gameShellPackages == packages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gameShellPackages = packages;
|
||||||
|
Q_EMIT gameShellPackagesChanged();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,12 @@
|
||||||
#include "waydroidapplicationlistmodel.h"
|
#include "waydroidapplicationlistmodel.h"
|
||||||
#include "waydroiddbusobject.h"
|
#include "waydroiddbusobject.h"
|
||||||
|
|
||||||
|
#include <KConfigWatcher>
|
||||||
|
#include <KSharedConfig>
|
||||||
|
|
||||||
#include <QCoroCore>
|
#include <QCoroCore>
|
||||||
#include <QCoroQmlTask>
|
#include <QCoroQmlTask>
|
||||||
|
#include <QDBusPendingCallWatcher>
|
||||||
#include <QDBusServiceWatcher>
|
#include <QDBusServiceWatcher>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
@ -32,6 +36,9 @@ class WaydroidDBusClient : public QObject
|
||||||
Q_PROPERTY(bool multiWindows READ multiWindows WRITE setMultiWindows NOTIFY multiWindowsChanged)
|
Q_PROPERTY(bool multiWindows READ multiWindows WRITE setMultiWindows NOTIFY multiWindowsChanged)
|
||||||
Q_PROPERTY(bool suspend READ suspend WRITE setSuspend NOTIFY suspendChanged)
|
Q_PROPERTY(bool suspend READ suspend WRITE setSuspend NOTIFY suspendChanged)
|
||||||
Q_PROPERTY(bool uevent READ uevent WRITE setUevent NOTIFY ueventChanged)
|
Q_PROPERTY(bool uevent READ uevent WRITE setUevent NOTIFY ueventChanged)
|
||||||
|
Q_PROPERTY(QString fakeTouch READ fakeTouch WRITE setFakeTouch NOTIFY fakeTouchChanged)
|
||||||
|
Q_PROPERTY(QString fakeWifi READ fakeWifi WRITE setFakeWifi NOTIFY fakeWifiChanged)
|
||||||
|
Q_PROPERTY(QStringList gameShellPackages READ gameShellPackages NOTIFY gameShellPackagesChanged)
|
||||||
Q_PROPERTY(WaydroidApplicationListModel *applicationListModel READ applicationListModel CONSTANT)
|
Q_PROPERTY(WaydroidApplicationListModel *applicationListModel READ applicationListModel CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
@ -98,16 +105,25 @@ public:
|
||||||
QCoro::QmlTask setSuspend(const bool suspend);
|
QCoro::QmlTask setSuspend(const bool suspend);
|
||||||
[[nodiscard]] bool uevent() const;
|
[[nodiscard]] bool uevent() const;
|
||||||
QCoro::QmlTask setUevent(const bool uevent);
|
QCoro::QmlTask setUevent(const bool uevent);
|
||||||
|
[[nodiscard]] QString fakeTouch() const;
|
||||||
|
QCoro::QmlTask setFakeTouch(const QString &fakeTouch);
|
||||||
|
[[nodiscard]] QString fakeWifi() const;
|
||||||
|
QCoro::QmlTask setFakeWifi(const QString &fakeWifi);
|
||||||
|
[[nodiscard]] QStringList gameShellPackages() const;
|
||||||
|
|
||||||
Q_INVOKABLE QCoro::QmlTask initialize(const SystemType systemType, const RomType romType, const bool forced = false);
|
Q_INVOKABLE QCoro::QmlTask initialize(const SystemType systemType, const RomType romType, const bool forced = false);
|
||||||
Q_INVOKABLE QCoro::QmlTask startSession();
|
Q_INVOKABLE QCoro::QmlTask startSession();
|
||||||
Q_INVOKABLE QCoro::QmlTask stopSession();
|
Q_INVOKABLE QCoro::QmlTask stopSession();
|
||||||
Q_INVOKABLE QCoro::QmlTask resetWaydroid();
|
Q_INVOKABLE QCoro::QmlTask resetWaydroid();
|
||||||
Q_INVOKABLE QCoro::QmlTask installApk(const QString apkFile);
|
Q_INVOKABLE QCoro::QmlTask installApk(const QString apkFile);
|
||||||
|
Q_INVOKABLE QCoro::QmlTask launchApplication(const QString appId);
|
||||||
Q_INVOKABLE QCoro::QmlTask deleteApplication(const QString appId);
|
Q_INVOKABLE QCoro::QmlTask deleteApplication(const QString appId);
|
||||||
|
Q_INVOKABLE QCoro::QmlTask refreshSupportsInfo();
|
||||||
Q_INVOKABLE QCoro::QmlTask refreshSessionInfo();
|
Q_INVOKABLE QCoro::QmlTask refreshSessionInfo();
|
||||||
Q_INVOKABLE QCoro::QmlTask refreshAndroidId();
|
Q_INVOKABLE QCoro::QmlTask refreshAndroidId();
|
||||||
Q_INVOKABLE QCoro::QmlTask refreshApplications();
|
Q_INVOKABLE QCoro::QmlTask refreshApplications();
|
||||||
|
Q_INVOKABLE bool gameShellEnabledForPackage(const QString &packageName) const;
|
||||||
|
Q_INVOKABLE void setGameShellEnabledForPackage(const QString &packageName, bool enabled);
|
||||||
|
|
||||||
Q_INVOKABLE void copyToClipboard(const QString text);
|
Q_INVOKABLE void copyToClipboard(const QString text);
|
||||||
|
|
||||||
|
|
@ -122,6 +138,9 @@ Q_SIGNALS:
|
||||||
void multiWindowsChanged();
|
void multiWindowsChanged();
|
||||||
void suspendChanged();
|
void suspendChanged();
|
||||||
void ueventChanged();
|
void ueventChanged();
|
||||||
|
void fakeTouchChanged();
|
||||||
|
void fakeWifiChanged();
|
||||||
|
void gameShellPackagesChanged();
|
||||||
|
|
||||||
void actionFinished(const QString message);
|
void actionFinished(const QString message);
|
||||||
void actionFailed(const QString message);
|
void actionFailed(const QString message);
|
||||||
|
|
@ -136,12 +155,14 @@ private Q_SLOTS:
|
||||||
void updateMultiWindows();
|
void updateMultiWindows();
|
||||||
void updateSuspend();
|
void updateSuspend();
|
||||||
void updateUevent();
|
void updateUevent();
|
||||||
|
void updateFakeTouch();
|
||||||
|
void updateFakeWifi();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OrgKdePlasmashellWaydroidInterface *m_interface;
|
OrgKdePlasmashellWaydroidInterface *m_interface;
|
||||||
QDBusServiceWatcher *m_watcher;
|
QDBusServiceWatcher *m_watcher;
|
||||||
|
|
||||||
Status m_status{NotInitialized};
|
Status m_status{NotSupported};
|
||||||
SessionStatus m_sessionStatus{SessionStopped};
|
SessionStatus m_sessionStatus{SessionStopped};
|
||||||
SystemType m_systemType{UnknownSystemType};
|
SystemType m_systemType{UnknownSystemType};
|
||||||
QString m_ipAddress{""};
|
QString m_ipAddress{""};
|
||||||
|
|
@ -152,22 +173,40 @@ private:
|
||||||
bool m_multiWindows{false};
|
bool m_multiWindows{false};
|
||||||
bool m_suspend{false};
|
bool m_suspend{false};
|
||||||
bool m_uevent{false};
|
bool m_uevent{false};
|
||||||
|
QString m_fakeTouch;
|
||||||
|
QString m_fakeWifi;
|
||||||
|
KSharedConfig::Ptr m_config;
|
||||||
|
KConfigWatcher::Ptr m_configWatcher;
|
||||||
|
QStringList m_gameShellPackages;
|
||||||
|
|
||||||
bool m_connected{false};
|
bool m_connected{false};
|
||||||
|
bool m_signalsConnected{false};
|
||||||
|
bool m_connectionCheckPending{false};
|
||||||
|
|
||||||
void connectSignals();
|
void connectSignals();
|
||||||
void initializeApplicationListModel();
|
void initializeApplicationListModel();
|
||||||
|
void checkWaydroidObject();
|
||||||
|
void handleUnavailableReply();
|
||||||
|
void onWaydroidObjectCheckFinished(QDBusPendingCallWatcher *watcher);
|
||||||
|
void resetState();
|
||||||
|
void scheduleWaydroidObjectCheck();
|
||||||
|
|
||||||
QCoro::Task<void> initializeTask(const SystemType systemType, const RomType romType, const bool forced = false);
|
QCoro::Task<void> initializeTask(const SystemType systemType, const RomType romType, const bool forced = false);
|
||||||
QCoro::Task<void> startSessionTask();
|
QCoro::Task<void> startSessionTask();
|
||||||
QCoro::Task<void> stopSessionTask();
|
QCoro::Task<void> stopSessionTask();
|
||||||
QCoro::Task<void> resetWaydroidTask();
|
QCoro::Task<void> resetWaydroidTask();
|
||||||
QCoro::Task<void> installApkTask(const QString apkFile);
|
QCoro::Task<void> installApkTask(const QString apkFile);
|
||||||
|
QCoro::Task<void> launchApplicationTask(const QString appId);
|
||||||
QCoro::Task<void> deleteApplicationTask(const QString appId);
|
QCoro::Task<void> deleteApplicationTask(const QString appId);
|
||||||
|
QCoro::Task<void> refreshSupportsInfoTask();
|
||||||
QCoro::Task<void> setMultiWindowsTask(const bool multiWindows);
|
QCoro::Task<void> setMultiWindowsTask(const bool multiWindows);
|
||||||
QCoro::Task<void> setSuspendTask(const bool suspend);
|
QCoro::Task<void> setSuspendTask(const bool suspend);
|
||||||
QCoro::Task<void> setUeventTask(const bool uevent);
|
QCoro::Task<void> setUeventTask(const bool uevent);
|
||||||
|
QCoro::Task<void> setFakeTouchTask(const QString &fakeTouch);
|
||||||
|
QCoro::Task<void> setFakeWifiTask(const QString &fakeWifi);
|
||||||
QCoro::Task<void> refreshSessionInfoTask();
|
QCoro::Task<void> refreshSessionInfoTask();
|
||||||
QCoro::Task<void> refreshAndroidIdTask();
|
QCoro::Task<void> refreshAndroidIdTask();
|
||||||
QCoro::Task<void> refreshApplicationsTask();
|
QCoro::Task<void> refreshApplicationsTask();
|
||||||
|
|
||||||
|
void reloadGameShellPackages();
|
||||||
};
|
};
|
||||||
|
|
@ -31,6 +31,8 @@ using namespace Qt::StringLiterals;
|
||||||
#define MULTI_WINDOWS_PROP_KEY "persist.waydroid.multi_windows"
|
#define MULTI_WINDOWS_PROP_KEY "persist.waydroid.multi_windows"
|
||||||
#define SUSPEND_PROP_KEY "persist.waydroid.suspend"
|
#define SUSPEND_PROP_KEY "persist.waydroid.suspend"
|
||||||
#define UEVENT_PROP_KEY "persist.waydroid.uevent"
|
#define UEVENT_PROP_KEY "persist.waydroid.uevent"
|
||||||
|
#define FAKE_TOUCH_PROP_KEY "persist.waydroid.fake_touch"
|
||||||
|
#define FAKE_WIFI_PROP_KEY "persist.waydroid.fake_wifi"
|
||||||
|
|
||||||
static const QRegularExpression sessionRegExp(u"Session:\\s*(\\w+)"_s);
|
static const QRegularExpression sessionRegExp(u"Session:\\s*(\\w+)"_s);
|
||||||
static const QRegularExpression ipAddressRegExp(u"IP address:\\s*(\\d+\\.\\d+\\.\\d+\\.\\d+)"_s);
|
static const QRegularExpression ipAddressRegExp(u"IP address:\\s*(\\d+\\.\\d+\\.\\d+\\.\\d+)"_s);
|
||||||
|
|
@ -179,10 +181,12 @@ void WaydroidDBusObject::stopSession()
|
||||||
|
|
||||||
connect(process, &QProcess::finished, this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) {
|
connect(process, &QProcess::finished, this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
Q_UNUSED(exitStatus);
|
Q_UNUSED(exitStatus);
|
||||||
|
const QByteArray errorLog = process->readAllStandardError();
|
||||||
process->deleteLater();
|
process->deleteLater();
|
||||||
|
|
||||||
if (exitCode == 0) {
|
if (exitCode != 0) {
|
||||||
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << process->readAllStandardError();
|
Q_EMIT errorOccurred(i18n("Failed to stop the Waydroid session."), QString::fromUtf8(errorLog));
|
||||||
|
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << errorLog;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,6 +254,27 @@ void WaydroidDBusObject::installApk(const QString apkFile)
|
||||||
process->start(WAYDROID_COMMAND, arguments);
|
process->start(WAYDROID_COMMAND, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusObject::launchApplication(const QString appId)
|
||||||
|
{
|
||||||
|
const QStringList arguments{u"app"_s, u"launch"_s, appId};
|
||||||
|
|
||||||
|
QProcess *process = new QProcess(this);
|
||||||
|
|
||||||
|
connect(process, &QProcess::finished, this, [this, appId, process](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
|
const QByteArray errorLog = process->readAllStandardError();
|
||||||
|
process->deleteLater();
|
||||||
|
|
||||||
|
if (exitCode == 0 && exitStatus == QProcess::NormalExit) {
|
||||||
|
Q_EMIT actionFinished(i18n("Application has been launched"));
|
||||||
|
} else {
|
||||||
|
Q_EMIT actionFailed(i18n("Application launch failed"));
|
||||||
|
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Error occurred while launching " << appId << ": " << errorLog;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process->start(WAYDROID_COMMAND, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
void WaydroidDBusObject::deleteApplication(const QString appId)
|
void WaydroidDBusObject::deleteApplication(const QString appId)
|
||||||
{
|
{
|
||||||
const QStringList arguments{u"app"_s, u"remove"_s, appId};
|
const QStringList arguments{u"app"_s, u"remove"_s, appId};
|
||||||
|
|
@ -375,6 +400,52 @@ void WaydroidDBusObject::setUevent(const bool uevent)
|
||||||
coro(this, value, uevent);
|
coro(this, value, uevent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString WaydroidDBusObject::fakeTouch() const
|
||||||
|
{
|
||||||
|
return m_fakeTouch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusObject::setFakeTouch(const QString &fakeTouch)
|
||||||
|
{
|
||||||
|
if (m_fakeTouch == fakeTouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto coro = [](WaydroidDBusObject *self, QString fakeTouch) -> QCoro::Task<void> {
|
||||||
|
QPointer<WaydroidDBusObject> guard(self);
|
||||||
|
if (co_await self->writePropValue(FAKE_TOUCH_PROP_KEY, fakeTouch)) {
|
||||||
|
if (guard) {
|
||||||
|
self->m_fakeTouch = fakeTouch;
|
||||||
|
Q_EMIT self->fakeTouchChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
coro(this, fakeTouch);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WaydroidDBusObject::fakeWifi() const
|
||||||
|
{
|
||||||
|
return m_fakeWifi;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaydroidDBusObject::setFakeWifi(const QString &fakeWifi)
|
||||||
|
{
|
||||||
|
if (m_fakeWifi == fakeWifi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto coro = [](WaydroidDBusObject *self, QString fakeWifi) -> QCoro::Task<void> {
|
||||||
|
QPointer<WaydroidDBusObject> guard(self);
|
||||||
|
if (co_await self->writePropValue(FAKE_WIFI_PROP_KEY, fakeWifi)) {
|
||||||
|
if (guard) {
|
||||||
|
self->m_fakeWifi = fakeWifi;
|
||||||
|
Q_EMIT self->fakeWifiChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
coro(this, fakeWifi);
|
||||||
|
}
|
||||||
|
|
||||||
QList<QDBusObjectPath> WaydroidDBusObject::applications() const
|
QList<QDBusObjectPath> WaydroidDBusObject::applications() const
|
||||||
{
|
{
|
||||||
QList<QDBusObjectPath> paths;
|
QList<QDBusObjectPath> paths;
|
||||||
|
|
@ -513,6 +584,7 @@ void WaydroidDBusObject::refreshAndroidId()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_androidId = "";
|
m_androidId = "";
|
||||||
|
Q_EMIT actionFailed(i18n("Failed to fetch Android ID"));
|
||||||
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << executeJob->error();
|
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << executeJob->error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -539,6 +611,12 @@ QCoro::Task<void> WaydroidDBusObject::refreshPropsInfo()
|
||||||
const QString ueventPropValue = co_await fetchPropValue(UEVENT_PROP_KEY, "false");
|
const QString ueventPropValue = co_await fetchPropValue(UEVENT_PROP_KEY, "false");
|
||||||
m_uevent = ueventPropValue == "true";
|
m_uevent = ueventPropValue == "true";
|
||||||
Q_EMIT ueventChanged();
|
Q_EMIT ueventChanged();
|
||||||
|
|
||||||
|
m_fakeTouch = co_await fetchPropValue(FAKE_TOUCH_PROP_KEY, QString{});
|
||||||
|
Q_EMIT fakeTouchChanged();
|
||||||
|
|
||||||
|
m_fakeWifi = co_await fetchPropValue(FAKE_WIFI_PROP_KEY, QString{});
|
||||||
|
Q_EMIT fakeWifiChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QCoro::Task<QString> WaydroidDBusObject::fetchPropValue(const QString key, const QString defaultValue)
|
QCoro::Task<QString> WaydroidDBusObject::fetchPropValue(const QString key, const QString defaultValue)
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ Q_SIGNALS:
|
||||||
Q_SCRIPTABLE void multiWindowsChanged();
|
Q_SCRIPTABLE void multiWindowsChanged();
|
||||||
Q_SCRIPTABLE void suspendChanged();
|
Q_SCRIPTABLE void suspendChanged();
|
||||||
Q_SCRIPTABLE void ueventChanged();
|
Q_SCRIPTABLE void ueventChanged();
|
||||||
|
Q_SCRIPTABLE void fakeTouchChanged();
|
||||||
|
Q_SCRIPTABLE void fakeWifiChanged();
|
||||||
|
|
||||||
Q_SCRIPTABLE void applicationAdded(QDBusObjectPath path);
|
Q_SCRIPTABLE void applicationAdded(QDBusObjectPath path);
|
||||||
Q_SCRIPTABLE void applicationRemoved(QDBusObjectPath path);
|
Q_SCRIPTABLE void applicationRemoved(QDBusObjectPath path);
|
||||||
|
|
@ -119,6 +121,10 @@ public Q_SLOTS:
|
||||||
Q_SCRIPTABLE void setSuspend(const bool suspend);
|
Q_SCRIPTABLE void setSuspend(const bool suspend);
|
||||||
Q_SCRIPTABLE bool uevent() const;
|
Q_SCRIPTABLE bool uevent() const;
|
||||||
Q_SCRIPTABLE void setUevent(const bool uevent);
|
Q_SCRIPTABLE void setUevent(const bool uevent);
|
||||||
|
Q_SCRIPTABLE QString fakeTouch() const;
|
||||||
|
Q_SCRIPTABLE void setFakeTouch(const QString &fakeTouch);
|
||||||
|
Q_SCRIPTABLE QString fakeWifi() const;
|
||||||
|
Q_SCRIPTABLE void setFakeWifi(const QString &fakeWifi);
|
||||||
Q_SCRIPTABLE QList<QDBusObjectPath> applications() const;
|
Q_SCRIPTABLE QList<QDBusObjectPath> applications() const;
|
||||||
|
|
||||||
Q_SCRIPTABLE void initialize(const int systemType, const int romType, const bool forced = false);
|
Q_SCRIPTABLE void initialize(const int systemType, const int romType, const bool forced = false);
|
||||||
|
|
@ -126,7 +132,9 @@ public Q_SLOTS:
|
||||||
Q_SCRIPTABLE void stopSession();
|
Q_SCRIPTABLE void stopSession();
|
||||||
Q_SCRIPTABLE void resetWaydroid();
|
Q_SCRIPTABLE void resetWaydroid();
|
||||||
Q_SCRIPTABLE void installApk(const QString apkFile);
|
Q_SCRIPTABLE void installApk(const QString apkFile);
|
||||||
|
Q_SCRIPTABLE void launchApplication(const QString appId);
|
||||||
Q_SCRIPTABLE void deleteApplication(const QString appId);
|
Q_SCRIPTABLE void deleteApplication(const QString appId);
|
||||||
|
Q_SCRIPTABLE void refreshSupportsInfo();
|
||||||
Q_SCRIPTABLE void refreshSessionInfo();
|
Q_SCRIPTABLE void refreshSessionInfo();
|
||||||
Q_SCRIPTABLE void refreshAndroidId();
|
Q_SCRIPTABLE void refreshAndroidId();
|
||||||
Q_SCRIPTABLE void refreshApplications();
|
Q_SCRIPTABLE void refreshApplications();
|
||||||
|
|
@ -143,8 +151,9 @@ private:
|
||||||
bool m_multiWindows{false};
|
bool m_multiWindows{false};
|
||||||
bool m_suspend{false};
|
bool m_suspend{false};
|
||||||
bool m_uevent{false};
|
bool m_uevent{false};
|
||||||
|
QString m_fakeTouch;
|
||||||
|
QString m_fakeWifi;
|
||||||
|
|
||||||
void refreshSupportsInfo();
|
|
||||||
void refreshInstallationInfo();
|
void refreshInstallationInfo();
|
||||||
QCoro::Task<void> refreshPropsInfo();
|
QCoro::Task<void> refreshPropsInfo();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,14 @@ ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
||||||
PATH settings
|
PATH settings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
||||||
|
qml/gaming/GameCenterOverlay.qml
|
||||||
|
qml/gaming/GamingHUD.qml
|
||||||
|
qml/gaming/GamingQuickSettings.qml
|
||||||
|
qml/gaming/RunningGamesView.qml
|
||||||
|
PATH gaming
|
||||||
|
)
|
||||||
|
|
||||||
target_link_libraries(org.kde.plasma.mobile.homescreen.folio PRIVATE
|
target_link_libraries(org.kde.plasma.mobile.homescreen.folio PRIVATE
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Qml
|
Qt::Qml
|
||||||
|
|
|
||||||
|
|
@ -706,12 +706,20 @@ MouseArea {
|
||||||
x: {
|
x: {
|
||||||
if (!targetDelegate) return 0
|
if (!targetDelegate) return 0
|
||||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||||
return Math.max(0, Math.min(Screen.width - width, delegateGlobal.x + (targetDelegate.width - width) / 2))
|
var win = targetDelegate.Window.window
|
||||||
|
var screenLeft = win && win.screen ? win.screen.virtualX : 0
|
||||||
|
var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
|
||||||
|
var centered = delegateGlobal.x + (targetDelegate.width - width) / 2
|
||||||
|
return Math.max(screenLeft, Math.min(screenRight - width, centered))
|
||||||
}
|
}
|
||||||
y: {
|
y: {
|
||||||
if (!targetDelegate) return 0
|
if (!targetDelegate) return 0
|
||||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||||
return Math.max(0, Math.min(Screen.height - height, delegateGlobal.y - height - Kirigami.Units.smallSpacing))
|
var win = targetDelegate.Window.window
|
||||||
|
var screenTop = win && win.screen ? win.screen.virtualY : 0
|
||||||
|
var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
|
||||||
|
var above = delegateGlobal.y - height - Kirigami.Units.smallSpacing
|
||||||
|
return Math.max(screenTop, Math.min(screenBottom - height, above))
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowingChanged: {
|
onShowingChanged: {
|
||||||
|
|
|
||||||
1389
containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml
Normal file
1389
containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml
Normal file
File diff suppressed because it is too large
Load diff
216
containments/homescreens/folio/qml/gaming/GamingHUD.qml
Normal file
216
containments/homescreens/folio/qml/gaming/GamingHUD.qml
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
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 {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal openRequested()
|
||||||
|
|
||||||
|
// Guard against startup timing where Kirigami units may briefly be 0/NaN.
|
||||||
|
// LayerShell surfaces must never be committed with zero size.
|
||||||
|
readonly property real safeGridUnit: ((Kirigami.Units.gridUnit || 0) > 0) ? Kirigami.Units.gridUnit : 16
|
||||||
|
|
||||||
|
property string toastMessage: ""
|
||||||
|
property bool toastError: false
|
||||||
|
readonly property bool toastActive: toastMessage.length > 0
|
||||||
|
|
||||||
|
// Most-recently-played game for quick resume. Populated from recentGames(1)
|
||||||
|
// and refreshed whenever the recent list changes.
|
||||||
|
property var quickResumeGame: null
|
||||||
|
readonly property bool hasQuickResume: quickResumeGame !== null
|
||||||
|
|
||||||
|
// Window grows leftward from top-right anchor:
|
||||||
|
// toast active → widest (needs room for message text)
|
||||||
|
// quick resume → medium (game name + controls)
|
||||||
|
// idle → compact (controls only)
|
||||||
|
width: toastActive ? safeGridUnit * 16 : (hasQuickResume ? safeGridUnit * 14 : safeGridUnit * 4)
|
||||||
|
height: toastActive ? safeGridUnit * 4 : safeGridUnit * 2
|
||||||
|
color: "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerShell.Window.scope: "gaming-hud"
|
||||||
|
LayerShell.Window.layer: LayerShell.Window.LayerOverlay
|
||||||
|
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorRight
|
||||||
|
LayerShell.Window.exclusionZone: 0
|
||||||
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||||
|
|
||||||
|
// Driven by the Loader in folio/qml/main.qml — set false to fade out
|
||||||
|
// before the Loader destroys the window.
|
||||||
|
property bool showing: true
|
||||||
|
|
||||||
|
opacity: showing ? 1 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: toastTimer
|
||||||
|
onTriggered: root.toastMessage = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GamingShell.GameLauncherProvider
|
||||||
|
function onGameLaunched(name) {
|
||||||
|
root.toastMessage = i18n("Launching %1", name)
|
||||||
|
root.toastError = false
|
||||||
|
toastTimer.interval = 3000
|
||||||
|
toastTimer.restart()
|
||||||
|
}
|
||||||
|
function onGameLaunchFailed(name, error) {
|
||||||
|
root.toastMessage = error.length > 0 ? error : i18n("Failed to launch %1", name)
|
||||||
|
root.toastError = true
|
||||||
|
toastTimer.interval = 5000
|
||||||
|
toastTimer.restart()
|
||||||
|
}
|
||||||
|
function onRecentGamesChanged() {
|
||||||
|
const recent = GamingShell.GameLauncherProvider.recentGames(1)
|
||||||
|
root.quickResumeGame = recent.length > 0 ? recent[0] : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const recent = GamingShell.GameLauncherProvider.recentGames(1)
|
||||||
|
root.quickResumeGame = recent.length > 0 ? recent[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- HUD pill (always visible, fills window width, grows leftward) ----
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
height: safeGridUnit * 2 - Kirigami.Units.smallSpacing * 2
|
||||||
|
radius: height / 2
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.55)
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
leftMargin: Kirigami.Units.smallSpacing
|
||||||
|
rightMargin: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// Quick-resume section — only visible when a recent game exists
|
||||||
|
QQC2.ToolButton {
|
||||||
|
visible: root.hasQuickResume
|
||||||
|
icon.name: "media-playback-start"
|
||||||
|
icon.color: "white"
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.text: root.quickResumeGame ? i18n("Resume %1", root.quickResumeGame.name) : ""
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
onClicked: {
|
||||||
|
if (root.quickResumeGame) {
|
||||||
|
GamingShell.GameLauncherProvider.launchByStorageId(root.quickResumeGame.storageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
visible: root.hasQuickResume
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.quickResumeGame ? root.quickResumeGame.name : ""
|
||||||
|
color: "white"
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
leftPadding: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator between quick-resume and controls
|
||||||
|
Rectangle {
|
||||||
|
visible: root.hasQuickResume
|
||||||
|
width: 1
|
||||||
|
implicitHeight: Kirigami.Units.gridUnit
|
||||||
|
color: Qt.rgba(1, 1, 1, 0.25)
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game Center button
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary gamepad battery
|
||||||
|
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
|
||||||
|
rightPadding: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- toast pill (slides in below HUD pill when active) ----
|
||||||
|
Rectangle {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
height: safeGridUnit * 2 - Kirigami.Units.smallSpacing * 2
|
||||||
|
radius: height / 2
|
||||||
|
color: root.toastError ? Qt.rgba(0.75, 0.1, 0.05, 0.9) : Qt.rgba(0, 0, 0, 0.55)
|
||||||
|
|
||||||
|
opacity: root.toastActive ? 1.0 : 0.0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
leftMargin: Kirigami.Units.largeSpacing
|
||||||
|
rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
}
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: root.toastError ? "dialog-warning" : "media-playback-start"
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.toastMessage
|
||||||
|
color: "white"
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,692 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
|
import org.kde.plasma.private.mobileshell as MobileShell
|
||||||
|
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
||||||
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
|
import org.kde.plasma.private.mobileshell.screenbrightnessplugin as ScreenBrightness
|
||||||
|
import org.kde.plasma.private.volume
|
||||||
|
import org.kde.plasma.networkmanagement as PlasmaNM
|
||||||
|
import org.kde.bluezqt 1.0 as BluezQt
|
||||||
|
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||||
|
import org.kde.plasma.quicksetting.nightcolor as NightColor
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
property bool opened: false
|
||||||
|
readonly property string acceptButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonA)
|
||||||
|
readonly property string closeButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonB)
|
||||||
|
|
||||||
|
function pulsePrimaryGamepad(lowIntensity, highIntensity, durationMs) {
|
||||||
|
var pad = GamingShell.GamepadManager.primaryGamepad
|
||||||
|
if (!pad || !pad.hasRumble) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pad.rumble(lowIntensity, highIntensity, durationMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focusable controls for gamepad navigation
|
||||||
|
property var _controls: []
|
||||||
|
property int _focusIndex: 0
|
||||||
|
|
||||||
|
function _buildControlsList() {
|
||||||
|
var list = []
|
||||||
|
if (GamingShell.PowerProfileControl.available && performanceSection._availableProfiles.length > 0) list.push(profileRow)
|
||||||
|
if (screenBrightness.brightnessAvailable) list.push(brightnessSlider)
|
||||||
|
if (PreferredDevice.sink) list.push(volumeSlider)
|
||||||
|
list.push(dndSwitch)
|
||||||
|
list.push(launchHintSwitch)
|
||||||
|
list.push(nightColorSwitch)
|
||||||
|
list.push(overlaySwitch)
|
||||||
|
list.push(wifiSwitch)
|
||||||
|
list.push(btSwitch)
|
||||||
|
list.push(airplaneSwitch)
|
||||||
|
_controls = list
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
opened = true
|
||||||
|
_buildControlsList()
|
||||||
|
_focusIndex = Math.max(0, Math.min(_focusIndex, _controls.length - 1))
|
||||||
|
_highlightCurrent()
|
||||||
|
pulsePrimaryGamepad(7000, 11000, 40)
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
opened = false
|
||||||
|
pulsePrimaryGamepad(5000, 8000, 30)
|
||||||
|
}
|
||||||
|
function toggle() {
|
||||||
|
if (opened) close(); else open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function _highlightCurrent() {
|
||||||
|
if (_controls.length > 0 && _focusIndex >= 0 && _focusIndex < _controls.length) {
|
||||||
|
_controls[_focusIndex].forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gamepad input handlers called from GameCenterOverlay
|
||||||
|
function gamepadUp() {
|
||||||
|
if (_focusIndex > 0) {
|
||||||
|
_focusIndex--
|
||||||
|
_highlightCurrent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gamepadDown() {
|
||||||
|
if (_focusIndex < _controls.length - 1) {
|
||||||
|
_focusIndex++
|
||||||
|
_highlightCurrent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gamepadLeft() {
|
||||||
|
var ctrl = _controls[_focusIndex]
|
||||||
|
if (typeof ctrl.decrease === "function") {
|
||||||
|
ctrl.decrease()
|
||||||
|
if (typeof ctrl.moved === "function") ctrl.moved()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gamepadRight() {
|
||||||
|
var ctrl = _controls[_focusIndex]
|
||||||
|
if (typeof ctrl.increase === "function") {
|
||||||
|
ctrl.increase()
|
||||||
|
if (typeof ctrl.moved === "function") ctrl.moved()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gamepadAccept() {
|
||||||
|
var ctrl = _controls[_focusIndex]
|
||||||
|
if (ctrl === profileRow) {
|
||||||
|
ctrl.increase()
|
||||||
|
pulsePrimaryGamepad(6000, 9000, 35)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ctrl instanceof QQC2.Switch) {
|
||||||
|
ctrl.toggle()
|
||||||
|
ctrl.toggled()
|
||||||
|
pulsePrimaryGamepad(6000, 9000, 35)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenedChanged: {
|
||||||
|
if (opened) {
|
||||||
|
_buildControlsList()
|
||||||
|
_focusIndex = Math.max(0, Math.min(_focusIndex, _controls.length - 1))
|
||||||
|
_highlightCurrent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eat clicks on the dimmed backdrop
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.opened
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dim backdrop
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
opacity: root.opened ? 0.4 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panel sliding in from the right
|
||||||
|
Rectangle {
|
||||||
|
id: panel
|
||||||
|
width: Math.min(root.width * 0.35, Kirigami.Units.gridUnit * 22)
|
||||||
|
height: root.height
|
||||||
|
anchors.top: root.top
|
||||||
|
anchors.bottom: root.bottom
|
||||||
|
|
||||||
|
x: root.opened ? root.width - width : root.width
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Theme.inherit: false
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||||
|
color: Qt.rgba(Kirigami.Theme.backgroundColor.r,
|
||||||
|
Kirigami.Theme.backgroundColor.g,
|
||||||
|
Kirigami.Theme.backgroundColor.b, 0.96)
|
||||||
|
|
||||||
|
// Subtle left border
|
||||||
|
Rectangle {
|
||||||
|
width: 1
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenBrightness.ScreenBrightnessUtil {
|
||||||
|
id: screenBrightness
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaNM.Handler {
|
||||||
|
id: nmHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaNM.EnabledConnections {
|
||||||
|
id: enabledConnections
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.largeSpacing * 2
|
||||||
|
contentHeight: settingsColumn.implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: settingsColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Kirigami.Units.largeSpacing * 2
|
||||||
|
|
||||||
|
// ---- Header ----
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
text: i18n("Quick Settings")
|
||||||
|
level: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "window-close-symbolic"
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// ---- Performance Profile ----
|
||||||
|
ColumnLayout {
|
||||||
|
id: performanceSection
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: GamingShell.PowerProfileControl.available
|
||||||
|
&& _availableProfiles.length > 0
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Performance")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordered low-to-high so gamepad left=slower, right=faster
|
||||||
|
readonly property var _profileOrder: ["power-saver", "balanced", "performance"]
|
||||||
|
readonly property var _availableProfiles: {
|
||||||
|
var ordered = []
|
||||||
|
for (var i = 0; i < _profileOrder.length; i++) {
|
||||||
|
if (GamingShell.PowerProfileControl.profiles.indexOf(_profileOrder[i]) >= 0) {
|
||||||
|
ordered.push(_profileOrder[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: profileRow
|
||||||
|
focus: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: profileButtons.implicitHeight
|
||||||
|
|
||||||
|
function decrease() {
|
||||||
|
var profiles = parent._availableProfiles
|
||||||
|
var idx = profiles.indexOf(GamingShell.PowerProfileControl.activeProfile)
|
||||||
|
if (idx > 0) {
|
||||||
|
GamingShell.PowerProfileControl.activeProfile = profiles[idx - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function increase() {
|
||||||
|
var profiles = parent._availableProfiles
|
||||||
|
var idx = profiles.indexOf(GamingShell.PowerProfileControl.activeProfile)
|
||||||
|
if (idx >= 0 && idx < profiles.length - 1) {
|
||||||
|
GamingShell.PowerProfileControl.activeProfile = profiles[idx + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -Kirigami.Units.smallSpacing
|
||||||
|
radius: Kirigami.Units.smallSpacing
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Kirigami.Theme.highlightColor
|
||||||
|
border.width: parent.activeFocus ? 2 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: profileButtons
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: performanceSection._availableProfiles
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "performance": return i18n("Performance")
|
||||||
|
case "balanced": return i18n("Balanced")
|
||||||
|
case "power-saver": return i18n("Power Saver")
|
||||||
|
default: return modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon.name: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "performance": return "speedometer"
|
||||||
|
case "balanced": return "system-suspend-hibernate"
|
||||||
|
case "power-saver": return "battery-profile-powersave"
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
highlighted: GamingShell.PowerProfileControl.activeProfile === modelData
|
||||||
|
onClicked: GamingShell.PowerProfileControl.activeProfile = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Brightness ----
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: screenBrightness.brightnessAvailable
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Brightness")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
source: "low-brightness"
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Slider {
|
||||||
|
id: brightnessSlider
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 1
|
||||||
|
to: screenBrightness.maxBrightness
|
||||||
|
stepSize: Math.max(1, Math.round(screenBrightness.maxBrightness / 20))
|
||||||
|
value: screenBrightness.brightness
|
||||||
|
onMoved: screenBrightness.brightness = value
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 0
|
||||||
|
running: true
|
||||||
|
repeat: false
|
||||||
|
onTriggered: brightnessSlider.value = Qt.binding(() => screenBrightness.brightness)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep Plasma/Kirigami colors while using a cleaner rounded style.
|
||||||
|
background: Rectangle {
|
||||||
|
x: brightnessSlider.leftPadding
|
||||||
|
y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2
|
||||||
|
width: brightnessSlider.availableWidth
|
||||||
|
height: Kirigami.Units.smallSpacing + 2
|
||||||
|
radius: height / 2
|
||||||
|
color: Kirigami.Theme.alternateBackgroundColor
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * brightnessSlider.visualPosition
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Kirigami.Theme.highlightColor
|
||||||
|
border.width: brightnessSlider.activeFocus ? 1 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Rectangle {
|
||||||
|
x: brightnessSlider.leftPadding + brightnessSlider.visualPosition * (brightnessSlider.availableWidth - width)
|
||||||
|
y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
radius: width / 2
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
border.color: brightnessSlider.pressed
|
||||||
|
? Kirigami.Theme.highlightColor
|
||||||
|
: Kirigami.Theme.disabledTextColor
|
||||||
|
border.width: brightnessSlider.activeFocus || brightnessSlider.pressed ? 2 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
source: "high-brightness"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Volume ----
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: PreferredDevice.sink != null
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Volume")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
source: "audio-volume-low"
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Slider {
|
||||||
|
id: volumeSlider
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: PulseAudio.MinimalVolume
|
||||||
|
to: PulseAudio.NormalVolume
|
||||||
|
stepSize: PulseAudio.NormalVolume / 20
|
||||||
|
value: PreferredDevice.sink ? PreferredDevice.sink.volume : 0
|
||||||
|
onMoved: {
|
||||||
|
if (PreferredDevice.sink) {
|
||||||
|
PreferredDevice.sink.volume = value
|
||||||
|
PreferredDevice.sink.muted = (value === 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep Plasma/Kirigami colors while using a cleaner rounded style.
|
||||||
|
background: Rectangle {
|
||||||
|
x: volumeSlider.leftPadding
|
||||||
|
y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
|
||||||
|
width: volumeSlider.availableWidth
|
||||||
|
height: Kirigami.Units.smallSpacing + 2
|
||||||
|
radius: height / 2
|
||||||
|
color: Kirigami.Theme.alternateBackgroundColor
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * volumeSlider.visualPosition
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Kirigami.Theme.highlightColor
|
||||||
|
border.width: volumeSlider.activeFocus ? 1 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Rectangle {
|
||||||
|
x: volumeSlider.leftPadding + volumeSlider.visualPosition * (volumeSlider.availableWidth - width)
|
||||||
|
y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
radius: width / 2
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
border.color: volumeSlider.pressed
|
||||||
|
? Kirigami.Theme.highlightColor
|
||||||
|
: Kirigami.Theme.disabledTextColor
|
||||||
|
border.width: volumeSlider.activeFocus || volumeSlider.pressed ? 2 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
source: "audio-volume-high"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// ---- Gaming Tweaks ----
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Gaming")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
columns: 2
|
||||||
|
rowSpacing: Kirigami.Units.smallSpacing
|
||||||
|
columnSpacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Switch {
|
||||||
|
id: dndSwitch
|
||||||
|
text: i18n("Do Not Disturb")
|
||||||
|
checked: MobileShellState.ShellDBusClient.doNotDisturb
|
||||||
|
onToggled: MobileShellState.ShellDBusClient.doNotDisturb = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Switch {
|
||||||
|
id: launchHintSwitch
|
||||||
|
text: i18n("Launch Hint")
|
||||||
|
checked: ShellSettings.Settings.gamingDismissHintEnabled
|
||||||
|
onToggled: ShellSettings.Settings.gamingDismissHintEnabled = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Switch {
|
||||||
|
id: nightColorSwitch
|
||||||
|
text: i18n("Night Color")
|
||||||
|
checked: NightColor.NightColorUtil.enabled
|
||||||
|
onToggled: NightColor.NightColorUtil.enabled = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Switch {
|
||||||
|
id: overlaySwitch
|
||||||
|
text: i18n("Perf Overlay")
|
||||||
|
checked: GamingShell.GameLauncherProvider.overlayEnabled
|
||||||
|
enabled: GamingShell.GameLauncherProvider.mangohudAvailable
|
||||||
|
opacity: enabled ? 1.0 : 0.5
|
||||||
|
onToggled: GamingShell.GameLauncherProvider.overlayEnabled = checked
|
||||||
|
|
||||||
|
QQC2.ToolTip.visible: !GamingShell.GameLauncherProvider.mangohudAvailable && hovered
|
||||||
|
QQC2.ToolTip.text: i18n("MangoHud is not installed")
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPS cap — spans both columns, driven by MangoHud fps_limit
|
||||||
|
QQC2.ButtonGroup { id: fpsCap; exclusive: true }
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.columnSpan: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
enabled: GamingShell.GameLauncherProvider.mangohudAvailable
|
||||||
|
opacity: enabled ? 1.0 : 0.5
|
||||||
|
|
||||||
|
PC3.Label { text: i18n("FPS Cap") }
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [
|
||||||
|
{ label: i18nc("@action:button FPS cap off", "Off"), fps: 0 },
|
||||||
|
{ label: "30", fps: 30 },
|
||||||
|
{ label: "40", fps: 40 },
|
||||||
|
{ label: "60", fps: 60 }
|
||||||
|
]
|
||||||
|
delegate: QQC2.Button {
|
||||||
|
required property var modelData
|
||||||
|
text: modelData.label
|
||||||
|
flat: true
|
||||||
|
checkable: true
|
||||||
|
checked: GamingShell.GameLauncherProvider.fpsLimit === modelData.fps
|
||||||
|
QQC2.ButtonGroup.group: fpsCap
|
||||||
|
onClicked: GamingShell.GameLauncherProvider.fpsLimit = modelData.fps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GameMode status (auto-managed, read-only indicator)
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: GamingShell.GameModeControl.available
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
source: "games-achievements"
|
||||||
|
}
|
||||||
|
PC3.Label {
|
||||||
|
text: GamingShell.GameModeControl.active
|
||||||
|
? i18n("GameMode requested")
|
||||||
|
: i18n("GameMode not requested")
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// ---- Connectivity toggles ----
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Connectivity")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
columns: 2
|
||||||
|
rowSpacing: Kirigami.Units.smallSpacing
|
||||||
|
columnSpacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
// WiFi toggle
|
||||||
|
QQC2.Switch {
|
||||||
|
id: wifiSwitch
|
||||||
|
text: i18n("Wi-Fi")
|
||||||
|
checked: enabledConnections.wirelessEnabled
|
||||||
|
onToggled: nmHandler.enableWireless(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bluetooth toggle
|
||||||
|
QQC2.Switch {
|
||||||
|
id: btSwitch
|
||||||
|
text: i18n("Bluetooth")
|
||||||
|
checked: !BluezQt.Manager.bluetoothBlocked
|
||||||
|
onToggled: BluezQt.Manager.bluetoothBlocked = !checked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Airplane mode
|
||||||
|
QQC2.Switch {
|
||||||
|
id: airplaneSwitch
|
||||||
|
text: i18n("Airplane Mode")
|
||||||
|
checked: PlasmaNM.Configuration.airplaneModeEnabled
|
||||||
|
onToggled: {
|
||||||
|
nmHandler.enableAirplaneMode(!PlasmaNM.Configuration.airplaneModeEnabled)
|
||||||
|
PlasmaNM.Configuration.airplaneModeEnabled = !PlasmaNM.Configuration.airplaneModeEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// ---- Battery info ----
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: MobileShell.BatteryInfo.isVisible
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Battery")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
MobileShell.BatteryIndicator {
|
||||||
|
textPixelSize: Kirigami.Units.gridUnit * 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: MobileShell.BatteryInfo.pluggedIn ? i18n("Charging") : ""
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Controller info ----
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: GamingShell.GamepadManager.hasGamepad
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Controllers")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: GamingShell.GamepadManager
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
required property string name
|
||||||
|
required property int battery
|
||||||
|
required property string type
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
source: "input-gaming"
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: name
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: battery >= 0 ? battery + "%" : i18n("Wired")
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// ---- Gamepad legend ----
|
||||||
|
PC3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: i18n("↕: Navigate ↔: Adjust %1: Toggle %2: Close",
|
||||||
|
acceptButtonLabel, closeButtonLabel)
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
|
||||||
|
opacity: 0.5
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillHeight: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
containments/homescreens/folio/qml/gaming/RunningGamesView.qml
Normal file
208
containments/homescreens/folio/qml/gaming/RunningGamesView.qml
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
|
import org.kde.taskmanager as TaskManager
|
||||||
|
|
||||||
|
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitHeight: taskList.count > 0 ? column.implicitHeight : 0
|
||||||
|
readonly property bool hasTasks: taskList.count > 0
|
||||||
|
readonly property int taskCount: taskList.count
|
||||||
|
|
||||||
|
signal taskActivated()
|
||||||
|
signal moveDownRequested()
|
||||||
|
|
||||||
|
function focusFirstTask() {
|
||||||
|
if (!hasTasks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskList.currentIndex = Math.max(0, taskList.currentIndex)
|
||||||
|
taskList.positionViewAtIndex(taskList.currentIndex, ListView.Visible)
|
||||||
|
taskList.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateCurrent() {
|
||||||
|
if (taskList.currentItem) {
|
||||||
|
taskList.currentItem.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCurrent() {
|
||||||
|
if (taskList.currentItem) {
|
||||||
|
var idx = taskList.currentIndex
|
||||||
|
tasks.requestClose(tasks.makeModelIndex(idx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskManager.VirtualDesktopInfo { id: vdInfo }
|
||||||
|
TaskManager.ActivityInfo { id: actInfo }
|
||||||
|
|
||||||
|
TaskManager.TasksModel {
|
||||||
|
id: tasks
|
||||||
|
filterByVirtualDesktop: true
|
||||||
|
filterByActivity: true
|
||||||
|
filterNotMaximized: false
|
||||||
|
filterByScreen: true
|
||||||
|
filterHidden: false
|
||||||
|
virtualDesktop: vdInfo.currentDesktop
|
||||||
|
activity: actInfo.currentActivity
|
||||||
|
groupMode: TaskManager.TasksModel.GroupApplications
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: column
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: taskList.count > 0
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
level: 2
|
||||||
|
text: i18n("Running")
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: taskList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: tasks
|
||||||
|
orientation: ListView.Horizontal
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
clip: true
|
||||||
|
keyNavigationEnabled: true
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
onActiveFocusChanged: {
|
||||||
|
if (activeFocus && count > 0 && currentIndex < 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onLeftPressed: {
|
||||||
|
if (count <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentIndex = Math.max(0, currentIndex - 1)
|
||||||
|
positionViewAtIndex(currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onRightPressed: {
|
||||||
|
if (count <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentIndex = Math.min(count - 1, currentIndex + 1)
|
||||||
|
positionViewAtIndex(currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: root.moveDownRequested()
|
||||||
|
Keys.onReturnPressed: currentItem && currentItem.activate()
|
||||||
|
Keys.onEnterPressed: currentItem && currentItem.activate()
|
||||||
|
|
||||||
|
delegate: QQC2.ItemDelegate {
|
||||||
|
id: taskItem
|
||||||
|
|
||||||
|
required property var decoration
|
||||||
|
required property var winIdList
|
||||||
|
|
||||||
|
width: Kirigami.Units.gridUnit * 8
|
||||||
|
height: Kirigami.Units.gridUnit * 5
|
||||||
|
|
||||||
|
readonly property var modelIndex: tasks.makeModelIndex(index)
|
||||||
|
readonly property bool isCurrent: ListView.isCurrentItem && taskList.activeFocus
|
||||||
|
readonly property string titleText: typeof model !== "undefined" && model.display ? model.display : ""
|
||||||
|
|
||||||
|
function activate() {
|
||||||
|
tasks.requestActivate(taskItem.modelIndex)
|
||||||
|
root.taskActivated()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
taskList.currentIndex = index
|
||||||
|
activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: activate()
|
||||||
|
Keys.onEnterPressed: activate()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: taskItem.isCurrent
|
||||||
|
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g,
|
||||||
|
Kirigami.Theme.highlightColor.b, 0.25)
|
||||||
|
: resumeArea.containsPress
|
||||||
|
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g,
|
||||||
|
Kirigami.Theme.highlightColor.b, 0.3)
|
||||||
|
: resumeArea.containsMouse
|
||||||
|
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g,
|
||||||
|
Kirigami.Theme.textColor.b, 0.1)
|
||||||
|
: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g,
|
||||||
|
Kirigami.Theme.textColor.b, 0.06)
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.large
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.large
|
||||||
|
source: taskItem.decoration
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: taskItem.titleText
|
||||||
|
maximumLineCount: 1
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button (top-right corner)
|
||||||
|
QQC2.ToolButton {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing / 2
|
||||||
|
width: Kirigami.Units.iconSizes.small
|
||||||
|
height: width
|
||||||
|
icon.name: "window-close-symbolic"
|
||||||
|
icon.width: Kirigami.Units.iconSizes.small
|
||||||
|
icon.height: Kirigami.Units.iconSizes.small
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
onClicked: tasks.requestClose(taskItem.modelIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: resumeArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
z: -1
|
||||||
|
// Leave room for the close button
|
||||||
|
onClicked: {
|
||||||
|
taskList.currentIndex = index
|
||||||
|
taskItem.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.state as MobileShellState
|
||||||
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
|
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
|
||||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
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.layershell 1.0 as LayerShell
|
||||||
import org.kde.plasma.private.sessions 2.0
|
import org.kde.plasma.private.sessions 2.0
|
||||||
|
|
@ -25,13 +26,84 @@ import org.kde.kirigamiaddons.components as KirigamiAddonsComponents
|
||||||
|
|
||||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||||
|
|
||||||
|
import "./gaming"
|
||||||
|
|
||||||
import "./private"
|
import "./private"
|
||||||
|
|
||||||
ContainmentItem {
|
ContainmentItem {
|
||||||
id: root
|
id: root
|
||||||
property Folio.HomeScreen folio: root.plasmoid
|
property Folio.HomeScreen folio: root.plasmoid
|
||||||
|
|
||||||
|
// Tracks whether the Game Center grid is visible within gaming mode.
|
||||||
|
// If gaming mode is already enabled at startup, open it immediately so
|
||||||
|
// the user is never left without controls.
|
||||||
|
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
|
||||||
|
onTriggered: root.showGameCenterHint = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ShellSettings.Settings
|
||||||
|
function onGamingModeEnabledChanged() {
|
||||||
|
root._applyGamingModeState(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: {
|
Component.onCompleted: {
|
||||||
|
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
|
||||||
folio.FolioSettings.load();
|
folio.FolioSettings.load();
|
||||||
folio.FavouritesModel.load();
|
folio.FavouritesModel.load();
|
||||||
folio.PageListModel.load();
|
folio.PageListModel.load();
|
||||||
|
|
@ -71,6 +143,18 @@ ContainmentItem {
|
||||||
screenGeometry: Plasmoid.containment.screenGeometry
|
screenGeometry: Plasmoid.containment.screenGeometry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In gaming mode, reopen Game Center when the last window goes away
|
||||||
|
// so the user is never stranded on a bare wallpaper.
|
||||||
|
Connections {
|
||||||
|
target: windowMaximizedTracker
|
||||||
|
enabled: ShellSettings.Settings.gamingModeEnabled
|
||||||
|
function onShowingWindowChanged() {
|
||||||
|
if (!windowMaximizedTracker.showingWindow && !root.gameCenterOpen) {
|
||||||
|
root.gameCenterOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Close app drawer when a new window appears
|
// Close app drawer when a new window appears
|
||||||
Connections {
|
Connections {
|
||||||
target: WindowPlugin.WindowUtil
|
target: WindowPlugin.WindowUtil
|
||||||
|
|
@ -89,6 +173,12 @@ ContainmentItem {
|
||||||
MobileShellState.ShellDBusClient.closeActionDrawer();
|
MobileShellState.ShellDBusClient.closeActionDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ShellSettings.Settings.gamingModeEnabled) {
|
||||||
|
// In gaming mode Home/Menu should reopen the Game Center overlay.
|
||||||
|
root.gameCenterOpen = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ShellSettings.Settings.convergenceModeEnabled) {
|
if (ShellSettings.Settings.convergenceModeEnabled) {
|
||||||
// Convergence: toggle the app drawer as a layer-shell overlay
|
// Convergence: toggle the app drawer as a layer-shell overlay
|
||||||
// without disturbing open windows.
|
// without disturbing open windows.
|
||||||
|
|
@ -186,7 +276,7 @@ ContainmentItem {
|
||||||
// task panel containment; this window only provides the visible dock.
|
// task panel containment; this window only provides the visible dock.
|
||||||
Window {
|
Window {
|
||||||
id: dockOverlay
|
id: dockOverlay
|
||||||
visible: ShellSettings.Settings.convergenceModeEnabled
|
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
width: Screen.width
|
width: Screen.width
|
||||||
height: Kirigami.Units.gridUnit * 3
|
height: Kirigami.Units.gridUnit * 3
|
||||||
|
|
@ -286,6 +376,7 @@ ContainmentItem {
|
||||||
Window {
|
Window {
|
||||||
id: drawerOverlay
|
id: drawerOverlay
|
||||||
visible: ShellSettings.Settings.convergenceModeEnabled
|
visible: ShellSettings.Settings.convergenceModeEnabled
|
||||||
|
&& !ShellSettings.Settings.gamingModeEnabled
|
||||||
&& folio.HomeScreenState.appDrawerOpenProgress > 0
|
&& folio.HomeScreenState.appDrawerOpenProgress > 0
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
width: Screen.width
|
width: Screen.width
|
||||||
|
|
@ -645,6 +736,74 @@ ContainmentItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Game Center overlay — full-screen grid of games shown when gaming mode
|
||||||
|
// is active. Sits at LayerTop so it covers running application windows
|
||||||
|
// without going above system notifications.
|
||||||
|
GameCenterOverlay {
|
||||||
|
id: gameCenterOverlay
|
||||||
|
folio: root.folio
|
||||||
|
visible: ShellSettings.Settings.gamingModeEnabled && root.gameCenterOpen
|
||||||
|
|
||||||
|
onGameStarted: root.gameCenterOpen = false
|
||||||
|
onDismissRequested: {
|
||||||
|
root.gameCenterOpen = false
|
||||||
|
if (ShellSettings.Settings.gamingDismissHintEnabled) {
|
||||||
|
root.showGameCenterHint = true
|
||||||
|
gameCenterHintTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small persistent button at the top-right corner of the screen that lets
|
||||||
|
// the user return to the Game Center after launching a game.
|
||||||
|
// Keep the Loader active for the full duration of gaming mode so the
|
||||||
|
// opacity Behavior in GamingHUD can animate both fade-in and fade-out.
|
||||||
|
//
|
||||||
|
// Hide the HUD while a game window covers the screen. A mapped LayerShell
|
||||||
|
// surface prevents KWin from using DRM direct scanout for the fullscreen
|
||||||
|
// game window. Setting showing=false triggers the opacity fade-out and then
|
||||||
|
// sets visible=false, which unmaps the Wayland surface and lets KWin bypass
|
||||||
|
// the compositor render loop entirely for the game frame.
|
||||||
|
Loader {
|
||||||
|
active: ShellSettings.Settings.gamingModeEnabled
|
||||||
|
sourceComponent: GamingHUD {
|
||||||
|
visible: showing
|
||||||
|
showing: !root.gameCenterOpen && !windowMaximizedTracker.showingWindow
|
||||||
|
onOpenRequested: root.gameCenterOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: gameCenterHint
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Kirigami.Units.gridUnit * 2
|
||||||
|
visible: root.showGameCenterHint && ShellSettings.Settings.gamingDismissHintEnabled
|
||||||
|
opacity: visible ? 1 : 0
|
||||||
|
z: 2000
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.65)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(1, 1, 1, 0.2)
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: hintText.implicitWidth + Kirigami.Units.gridUnit * 2
|
||||||
|
implicitHeight: hintText.implicitHeight + Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
id: hintText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: i18n("Gaming mode is still on. Use Home or the gamepad icon to reopen Game Center.")
|
||||||
|
color: "white"
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: Math.min(root.width * 0.8, Kirigami.Units.gridUnit * 30)
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MobileShell.HomeScreen {
|
MobileShell.HomeScreen {
|
||||||
id: homeScreen
|
id: homeScreen
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ Item {
|
||||||
context: Qt.ApplicationShortcut
|
context: Qt.ApplicationShortcut
|
||||||
onActivated: {
|
onActivated: {
|
||||||
if (drawer.actionDrawer.intendedToBeVisible) {
|
if (drawer.actionDrawer.intendedToBeVisible) {
|
||||||
|
drawer.actionDrawer.intendedToBeVisible = false;
|
||||||
drawer.actionDrawer.close();
|
drawer.actionDrawer.close();
|
||||||
} else {
|
} else {
|
||||||
drawer.actionDrawer.openToPinnedMode = false;
|
drawer.actionDrawer.openToPinnedMode = false;
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,17 @@ ContainmentItem {
|
||||||
// Whether the startup feedback is showing
|
// Whether the startup feedback is showing
|
||||||
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback
|
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback
|
||||||
|
|
||||||
|
readonly property bool gamingMode: ShellSettings.Settings.gamingModeEnabled
|
||||||
|
|
||||||
// Whether an app is maximized and showing (does not include startup feedback)
|
// Whether an app is maximized and showing (does not include startup feedback)
|
||||||
readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback
|
readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback
|
||||||
|
|
||||||
// Whether the currently showing app is in "fullscreen"
|
// Whether the currently showing app is in "fullscreen"
|
||||||
readonly property bool fullscreen: {
|
readonly property bool fullscreen: {
|
||||||
|
if (gamingMode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// In convergence mode the status bar is always visible, like a desktop panel.
|
// In convergence mode the status bar is always visible, like a desktop panel.
|
||||||
if (ShellSettings.Settings.convergenceModeEnabled) {
|
if (ShellSettings.Settings.convergenceModeEnabled) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -69,7 +75,7 @@ ContainmentItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real panelHeight: MobileShell.Constants.topPanelHeight
|
readonly property real panelHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight
|
||||||
onPanelHeightChanged: setWindowProperties()
|
onPanelHeightChanged: setWindowProperties()
|
||||||
|
|
||||||
function setWindowProperties() {
|
function setWindowProperties() {
|
||||||
|
|
@ -123,6 +129,11 @@ ContainmentItem {
|
||||||
function onConvergenceModeEnabledChanged() {
|
function onConvergenceModeEnabledChanged() {
|
||||||
root.setWindowProperties();
|
root.setWindowProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onGamingModeEnabledChanged() {
|
||||||
|
root.setWindowProperties();
|
||||||
|
MobileShellState.ShellDBusClient.panelState = ShellSettings.Settings.gamingModeEnabled ? "hidden" : (fullscreen ? "hidden" : "default");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|
@ -136,22 +147,23 @@ ContainmentItem {
|
||||||
// MaximizeArea by the panel height.
|
// MaximizeArea by the panel height.
|
||||||
Window {
|
Window {
|
||||||
id: topBarSpaceReserver
|
id: topBarSpaceReserver
|
||||||
visible: ShellSettings.Settings.convergenceModeEnabled
|
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
||||||
height: root.panelHeight
|
height: Math.max(1, root.panelHeight)
|
||||||
width: 1
|
width: 1
|
||||||
|
|
||||||
LayerShell.Window.scope: "topbar-space"
|
LayerShell.Window.scope: "topbar-space"
|
||||||
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
||||||
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
||||||
LayerShell.Window.exclusionZone: root.panelHeight
|
LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight)
|
||||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visual panel component
|
// Visual panel component
|
||||||
StatusPanel {
|
StatusPanel {
|
||||||
id: statusPanel
|
id: statusPanel
|
||||||
|
visible: !ShellSettings.Settings.gamingModeEnabled
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
containmentItem: root
|
containmentItem: root
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,14 @@ ContainmentItem {
|
||||||
|
|
||||||
readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height)
|
readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height)
|
||||||
|
|
||||||
readonly property real navigationPanelHeight: MobileShell.Constants.navigationPanelThickness
|
readonly property bool gamingMode: ShellSettings.Settings.gamingModeEnabled
|
||||||
|
|
||||||
|
readonly property real navigationPanelHeight: gamingMode ? 0 : MobileShell.Constants.navigationPanelThickness
|
||||||
onNavigationPanelHeightChanged: setWindowProperties()
|
onNavigationPanelHeightChanged: setWindowProperties()
|
||||||
|
|
||||||
readonly property real intendedWindowThickness: navigationPanelHeight
|
readonly property real intendedWindowThickness: navigationPanelHeight
|
||||||
readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width
|
readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width
|
||||||
readonly property real intendedWindowOffset: inLandscape ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
|
readonly property real intendedWindowOffset: (inLandscape && !gamingMode) ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
|
||||||
readonly property int intendedWindowLocation: inLandscape ? PlasmaCore.Types.RightEdge : PlasmaCore.Types.BottomEdge
|
readonly property int intendedWindowLocation: inLandscape ? PlasmaCore.Types.RightEdge : PlasmaCore.Types.BottomEdge
|
||||||
|
|
||||||
onIntendedWindowLengthChanged: maximizeTimer.restart() // ensure it always takes up the full length of the screen
|
onIntendedWindowLengthChanged: maximizeTimer.restart() // ensure it always takes up the full length of the screen
|
||||||
|
|
@ -136,6 +138,11 @@ ContainmentItem {
|
||||||
function onConvergenceModeEnabledChanged() {
|
function onConvergenceModeEnabledChanged() {
|
||||||
root.setWindowProperties();
|
root.setWindowProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onGamingModeEnabledChanged() {
|
||||||
|
root.setWindowProperties();
|
||||||
|
navigationPanel.offset = ShellSettings.Settings.gamingModeEnabled ? MobileShell.Constants.navigationPanelThickness : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: setWindowProperties();
|
Component.onCompleted: setWindowProperties();
|
||||||
|
|
@ -153,18 +160,19 @@ ContainmentItem {
|
||||||
Window {
|
Window {
|
||||||
id: dockSpaceReserver
|
id: dockSpaceReserver
|
||||||
visible: ShellSettings.Settings.convergenceModeEnabled
|
visible: ShellSettings.Settings.convergenceModeEnabled
|
||||||
|
&& !ShellSettings.Settings.gamingModeEnabled
|
||||||
&& !(ShellSettings.Settings.autoHidePanelsEnabled
|
&& !(ShellSettings.Settings.autoHidePanelsEnabled
|
||||||
&& windowMaximizedTracker.showingWindow)
|
&& windowMaximizedTracker.showingWindow)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
||||||
// height is set by layer-shell anchoring; provide a fallback.
|
// height is set by layer-shell anchoring; provide a fallback.
|
||||||
height: Kirigami.Units.gridUnit * 3
|
height: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
||||||
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
|
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
|
||||||
|
|
||||||
LayerShell.Window.scope: "dock-space"
|
LayerShell.Window.scope: "dock-space"
|
||||||
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
||||||
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
||||||
LayerShell.Window.exclusionZone: Kirigami.Units.gridUnit * 3
|
LayerShell.Window.exclusionZone: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
||||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +185,9 @@ ContainmentItem {
|
||||||
return (windowMaximizedTracker.showingWindow || isCurrentWindowFullscreen) && !showingStartupFeedback
|
return (windowMaximizedTracker.showingWindow || isCurrentWindowFullscreen) && !showingStartupFeedback
|
||||||
}
|
}
|
||||||
readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen
|
readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen
|
||||||
readonly property bool fullscreen: isCurrentWindowFullscreen || (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)
|
readonly property bool fullscreen: ShellSettings.Settings.gamingModeEnabled
|
||||||
|
|| isCurrentWindowFullscreen
|
||||||
|
|| (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)
|
||||||
|
|
||||||
WindowPlugin.WindowMaximizedTracker {
|
WindowPlugin.WindowMaximizedTracker {
|
||||||
id: windowMaximizedTracker
|
id: windowMaximizedTracker
|
||||||
|
|
@ -205,6 +215,7 @@ ContainmentItem {
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: navigationPanel
|
id: navigationPanel
|
||||||
|
visible: !ShellSettings.Settings.gamingModeEnabled
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
property real offset: 0
|
property real offset: 0
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,45 @@ import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
|
||||||
KCM.SimpleKCM {
|
KCM.SimpleKCM {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
function packagePatterns(value: string): var {
|
||||||
|
if (value === "") {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.split(",")
|
||||||
|
.map(pattern => pattern.trim())
|
||||||
|
.filter(pattern => pattern !== "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function wildcardRegExp(pattern: string): RegExp {
|
||||||
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
||||||
|
return new RegExp("^" + escaped.replace(/\*/g, ".*") + "$")
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExactPackage(value: string, packageName: string): bool {
|
||||||
|
return packagePatterns(value).includes(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasWildcardPackage(value: string, packageName: string): bool {
|
||||||
|
return packagePatterns(value)
|
||||||
|
.filter(pattern => pattern.includes("*"))
|
||||||
|
.some(pattern => wildcardRegExp(pattern).test(packageName))
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEffectivePackage(value: string, packageName: string): bool {
|
||||||
|
return hasExactPackage(value, packageName) || hasWildcardPackage(value, packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExactPackage(value: string, packageName: string, enabled: bool): string {
|
||||||
|
const updatedPatterns = packagePatterns(value).filter(pattern => pattern !== packageName)
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
updatedPatterns.push(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPatterns.join(",")
|
||||||
|
}
|
||||||
|
|
||||||
topPadding: Kirigami.Units.largeSpacing
|
topPadding: Kirigami.Units.largeSpacing
|
||||||
bottomPadding: Kirigami.Units.largeSpacing
|
bottomPadding: Kirigami.Units.largeSpacing
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
|
|
@ -74,6 +113,7 @@ KCM.SimpleKCM {
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
Kirigami.InlineMessage {
|
Kirigami.InlineMessage {
|
||||||
|
|
@ -85,6 +125,11 @@ KCM.SimpleKCM {
|
||||||
showCloseButton: true
|
showCloseButton: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kirigami.PlaceholderMessage {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
explanation: i18n("This page manages the launchers exported by Waydroid. Enable Show in Game Shell for Android apps you want listed in Game Center's Waydroid tab. Touch and Wi-Fi toggles add or remove exact package names from Waydroid's documented compatibility property lists. Wildcard rules remain in the main Waydroid properties page.")
|
||||||
|
}
|
||||||
|
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: AIP.WaydroidDBusClient.applicationListModel
|
model: AIP.WaydroidDBusClient.applicationListModel
|
||||||
|
|
@ -95,13 +140,30 @@ KCM.SimpleKCM {
|
||||||
width: ListView.view.width
|
width: ListView.view.width
|
||||||
|
|
||||||
background: null
|
background: null
|
||||||
contentItem: RowLayout {
|
contentItem: ColumnLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: model.name
|
text: model.name
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
text: i18nc("@action:button", "Launch the application")
|
||||||
|
icon.name: "media-playback-start"
|
||||||
|
|
||||||
|
onClicked: AIP.WaydroidDBusClient.launchApplication(model.id)
|
||||||
|
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
text: i18nc("@action:button", "Delete the application")
|
text: i18nc("@action:button", "Delete the application")
|
||||||
|
|
@ -114,6 +176,71 @@ KCM.SimpleKCM {
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
QQC2.CheckBox {
|
||||||
|
text: i18n("Show in Game Shell")
|
||||||
|
checked: AIP.WaydroidDBusClient.gameShellPackages.indexOf(model.id) !== -1
|
||||||
|
|
||||||
|
onClicked: AIP.WaydroidDBusClient.setGameShellEnabledForPackage(model.id, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
text: i18n("Adds this app to the Game Shell allowlist so it appears in Game Center under Waydroid.")
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
QQC2.CheckBox {
|
||||||
|
id: fakeTouchToggle
|
||||||
|
text: i18n("Touch")
|
||||||
|
checked: root.hasEffectivePackage(AIP.WaydroidDBusClient.fakeTouch, model.id)
|
||||||
|
enabled: !root.hasWildcardPackage(AIP.WaydroidDBusClient.fakeTouch, model.id)
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
AIP.WaydroidDBusClient.fakeTouch = root.updateExactPackage(AIP.WaydroidDBusClient.fakeTouch, model.id, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.visible: hovered && !enabled
|
||||||
|
QQC2.ToolTip.text: i18n("Managed by a wildcard pattern in Waydroid properties")
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.CheckBox {
|
||||||
|
id: fakeWifiToggle
|
||||||
|
text: i18n("Wi-Fi")
|
||||||
|
checked: root.hasEffectivePackage(AIP.WaydroidDBusClient.fakeWifi, model.id)
|
||||||
|
enabled: !root.hasWildcardPackage(AIP.WaydroidDBusClient.fakeWifi, model.id)
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
AIP.WaydroidDBusClient.fakeWifi = root.updateExactPackage(AIP.WaydroidDBusClient.fakeWifi, model.id, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.visible: hovered && !enabled
|
||||||
|
QQC2.ToolTip.text: i18n("Managed by a wildcard pattern in Waydroid properties")
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !fakeTouchToggle.enabled || !fakeWifiToggle.enabled
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
text: i18n("One or more compatibility settings for this app come from a wildcard rule. Edit the global Waydroid property to change that rule.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -121,4 +248,22 @@ KCM.SimpleKCM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
visible: AIP.WaydroidDBusClient.status !== AIP.WaydroidDBusClient.Initialized
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18n("Waydroid is unavailable")
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Button {
|
||||||
|
text: i18n("Check again")
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
|
||||||
|
&& AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionRunning
|
||||||
|
|
||||||
|
function packagePatternSummary(value: string): string {
|
||||||
|
return value === "" ? i18n("Not set") : value
|
||||||
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
title: i18n("General information")
|
title: i18n("General information")
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +73,64 @@ ColumnLayout {
|
||||||
onAccepted: AIP.WaydroidDBusClient.resetWaydroid()
|
onAccepted: AIP.WaydroidDBusClient.resetWaydroid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kirigami.PromptDialog {
|
||||||
|
id: fakeTouchDialog
|
||||||
|
title: i18n("Touch input override")
|
||||||
|
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
fakeTouchField.text = AIP.WaydroidDBusClient.fakeTouch
|
||||||
|
fakeTouchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: AIP.WaydroidDBusClient.fakeTouch = fakeTouchField.text.trim()
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: i18n("Comma-separated package names for apps where mouse input should be interpreted as touch. Supports * wildcards. Leave empty to clear the override.")
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.TextField {
|
||||||
|
id: fakeTouchField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "com.rovio.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.PromptDialog {
|
||||||
|
id: fakeWifiDialog
|
||||||
|
title: i18n("Wi-Fi override")
|
||||||
|
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
fakeWifiField.text = AIP.WaydroidDBusClient.fakeWifi
|
||||||
|
fakeWifiField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: AIP.WaydroidDBusClient.fakeWifi = fakeWifiField.text.trim()
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: i18n("Comma-separated package names for apps that should always appear to be on Wi-Fi. Supports * wildcards. Leave empty to clear the override.")
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.TextField {
|
||||||
|
id: fakeWifiField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "com.gameloft.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some information such as IP address can take time to be set by Waydroid
|
// Some information such as IP address can take time to be set by Waydroid
|
||||||
|
|
@ -140,5 +205,29 @@ ColumnLayout {
|
||||||
infoMessage.visible = true
|
infoMessage.visible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator { above: uevent; below: fakeTouch }
|
||||||
|
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
id: fakeTouch
|
||||||
|
text: i18n("Touch input override")
|
||||||
|
description: root.packagePatternSummary(AIP.WaydroidDBusClient.fakeTouch)
|
||||||
|
trailing: PC3.Button {
|
||||||
|
text: i18n("Edit")
|
||||||
|
onClicked: fakeTouchDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator { above: fakeTouch; below: fakeWifi }
|
||||||
|
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
id: fakeWifi
|
||||||
|
text: i18n("Wi-Fi override")
|
||||||
|
description: root.packagePatternSummary(AIP.WaydroidDBusClient.fakeWifi)
|
||||||
|
trailing: PC3.Button {
|
||||||
|
text: i18n("Edit")
|
||||||
|
onClicked: fakeWifiDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ KCM.SimpleKCM {
|
||||||
}
|
}
|
||||||
|
|
||||||
WaydroidLoader {
|
WaydroidLoader {
|
||||||
visible: AIP.WaydroidDBusClient.androidId === ""
|
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
|
||||||
|
&& AIP.WaydroidDBusClient.androidId === ""
|
||||||
|
&& !inlineMessage.visible
|
||||||
text: i18n("Fetching your Android ID.\nIt can take a few seconds.")
|
text: i18n("Fetching your Android ID.\nIt can take a few seconds.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +55,8 @@ KCM.SimpleKCM {
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
visible: AIP.WaydroidDBusClient.androidId !== ""
|
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
|
||||||
|
&& AIP.WaydroidDBusClient.androidId !== ""
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent
|
anchors.left: parent
|
||||||
anchors.leftMargin: Kirigami.Units.largeSpacing
|
anchors.leftMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
@ -76,4 +79,22 @@ KCM.SimpleKCM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
visible: AIP.WaydroidDBusClient.status !== AIP.WaydroidDBusClient.Initialized
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18n("Waydroid is unavailable")
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
text: i18n("Check again")
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,14 @@ KCM.SimpleKCM {
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
text: i18n("Waydroid is not installed")
|
text: i18n("Waydroid is unavailable")
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PC3.Button {
|
PC3.Button {
|
||||||
text: i18n("Check installation")
|
text: i18n("Check again")
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
|
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ Loader {
|
||||||
|
|
||||||
property var currentWindow
|
property var currentWindow
|
||||||
|
|
||||||
// Window that needs geometry clamping after un-maximize in convergence
|
// Windows awaiting geometry clamping after un-maximize in convergence
|
||||||
// mode. Set in onMaximizedChanged and consumed by the timer below.
|
// mode. Using an array so concurrent un-maximizes are not lost.
|
||||||
property var pendingConstrainWindow: null
|
property var pendingConstrainWindows: []
|
||||||
|
|
||||||
// After a window is un-maximized in convergence mode, the dockSpaceReserver
|
// After a window is un-maximized in convergence mode, the dockSpaceReserver
|
||||||
// LayerShell surface needs one Wayland roundtrip to (re)commit its exclusive
|
// LayerShell surface needs one Wayland roundtrip to (re)commit its exclusive
|
||||||
|
|
@ -24,15 +24,17 @@ Loader {
|
||||||
id: constrainAfterRestoreTimer
|
id: constrainAfterRestoreTimer
|
||||||
interval: 200
|
interval: 200
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
const window = root.pendingConstrainWindow
|
const windows = root.pendingConstrainWindows.slice()
|
||||||
root.pendingConstrainWindow = null
|
root.pendingConstrainWindows = []
|
||||||
if (!window || window.deleted || !window.normalWindow) return
|
for (const window of windows) {
|
||||||
if (!ShellSettings.Settings.convergenceModeEnabled) return
|
if (!window || window.deleted || !window.normalWindow) continue
|
||||||
|
if (!ShellSettings.Settings.convergenceModeEnabled) continue
|
||||||
|
if (ShellSettings.Settings.gamingModeEnabled) continue
|
||||||
|
|
||||||
const output = window.output
|
const output = window.output
|
||||||
const desktop = window.desktops[0]
|
const desktop = window.desktops[0]
|
||||||
if (!output) return
|
if (!output) continue
|
||||||
if (!desktop) return
|
if (!desktop) continue
|
||||||
|
|
||||||
const maxRect = KWinComponents.Workspace.clientArea(
|
const maxRect = KWinComponents.Workspace.clientArea(
|
||||||
KWinComponents.Workspace.MaximizeArea, output, desktop)
|
KWinComponents.Workspace.MaximizeArea, output, desktop)
|
||||||
|
|
@ -48,6 +50,7 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function run(window) {
|
function run(window) {
|
||||||
// HACK: don't maximize xwaylandvideobridge
|
// HACK: don't maximize xwaylandvideobridge
|
||||||
|
|
@ -60,6 +63,12 @@ Loader {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ShellSettings.Settings.gamingModeEnabled) {
|
||||||
|
window.noBorder = true;
|
||||||
|
window.setMaximize(true, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ShellSettings.Settings.convergenceModeEnabled) {
|
if (ShellSettings.Settings.convergenceModeEnabled) {
|
||||||
window.noBorder = false;
|
window.noBorder = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -91,6 +100,9 @@ Loader {
|
||||||
target: currentWindow
|
target: currentWindow
|
||||||
|
|
||||||
function onFullScreenChanged() {
|
function onFullScreenChanged() {
|
||||||
|
if (!currentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentWindow.interactiveMoveResizeFinished.connect((currentWindow) => {
|
currentWindow.interactiveMoveResizeFinished.connect((currentWindow) => {
|
||||||
root.run(currentWindow);
|
root.run(currentWindow);
|
||||||
});
|
});
|
||||||
|
|
@ -98,6 +110,9 @@ Loader {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMaximizedChanged() {
|
function onMaximizedChanged() {
|
||||||
|
if (!currentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!currentWindow.maximizable) {
|
if (!currentWindow.maximizable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +125,7 @@ Loader {
|
||||||
// zone is re-committed over a Wayland roundtrip.
|
// zone is re-committed over a Wayland roundtrip.
|
||||||
if (ShellSettings.Settings.convergenceModeEnabled
|
if (ShellSettings.Settings.convergenceModeEnabled
|
||||||
&& ShellSettings.Settings.autoHidePanelsEnabled) {
|
&& ShellSettings.Settings.autoHidePanelsEnabled) {
|
||||||
root.pendingConstrainWindow = currentWindow
|
root.pendingConstrainWindows.push(currentWindow)
|
||||||
constrainAfterRestoreTimer.restart()
|
constrainAfterRestoreTimer.restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,12 +143,25 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onGamingModeEnabledChanged() {
|
||||||
|
const windows = KWinComponents.Workspace.windows;
|
||||||
|
|
||||||
|
for (let i = 0; i < windows.length; i++) {
|
||||||
|
if (windows[i].normalWindow) {
|
||||||
|
root.run(windows[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: KWinComponents.Workspace
|
target: KWinComponents.Workspace
|
||||||
|
|
||||||
function onWindowAdded(window) {
|
function onWindowAdded(window) {
|
||||||
|
if (!window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (window.normalWindow) {
|
if (window.normalWindow) {
|
||||||
window.interactiveMoveResizeFinished.connect((window) => {
|
window.interactiveMoveResizeFinished.connect((window) => {
|
||||||
root.run(window);
|
root.run(window);
|
||||||
|
|
@ -143,6 +171,9 @@ Loader {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowActivated(window) {
|
function onWindowActivated(window) {
|
||||||
|
if (!window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (window.normalWindow) {
|
if (window.normalWindow) {
|
||||||
currentWindow = window;
|
currentWindow = window;
|
||||||
window.interactiveMoveResizeFinished.connect((window) => {
|
window.interactiveMoveResizeFinished.connect((window) => {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ plasma_install_package(bluetooth org.kde.plasma.quicksetting.bluetooth quicksett
|
||||||
plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings)
|
plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings)
|
||||||
plasma_install_package(docked org.kde.plasma.quicksetting.docked quicksettings)
|
plasma_install_package(docked org.kde.plasma.quicksetting.docked quicksettings)
|
||||||
plasma_install_package(donotdisturb org.kde.plasma.quicksetting.donotdisturb quicksettings)
|
plasma_install_package(donotdisturb org.kde.plasma.quicksetting.donotdisturb quicksettings)
|
||||||
|
plasma_install_package(gaming org.kde.plasma.quicksetting.gaming quicksettings)
|
||||||
|
plasma_install_package(gaminghint org.kde.plasma.quicksetting.gaminghint quicksettings)
|
||||||
plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings)
|
plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings)
|
||||||
plasma_install_package(keyboardtoggle org.kde.plasma.quicksetting.keyboardtoggle quicksettings)
|
plasma_install_package(keyboardtoggle org.kde.plasma.quicksetting.keyboardtoggle quicksettings)
|
||||||
plasma_install_package(kscreenosd org.kde.plasma.quicksetting.kscreenosd quicksettings)
|
plasma_install_package(kscreenosd org.kde.plasma.quicksetting.kscreenosd quicksettings)
|
||||||
|
|
|
||||||
16
quicksettings/gaming/Messages.sh
Normal file
16
quicksettings/gaming/Messages.sh
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
# SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
set -e
|
||||||
|
: "${XGETTEXT:?XGETTEXT is not set}"
|
||||||
|
: "${podir:?podir is not set}"
|
||||||
|
|
||||||
|
mapfile -t files < <(find . \( -name '*.js' -o -name '*.qml' -o -name '*.cpp' \) -print)
|
||||||
|
if [[ ${#files[@]} -eq 0 ]]; then
|
||||||
|
echo "Messages.sh: no input files found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$XGETTEXT" "${files[@]}" -o "$podir/plasma_org.kde.plasma.quicksetting.gaming.pot"
|
||||||
58
quicksettings/gaming/contents/ui/main.qml
Normal file
58
quicksettings/gaming/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
|
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||||
|
|
||||||
|
QS.QuickSetting {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
text: i18n("Gaming Mode")
|
||||||
|
icon: "input-gaming"
|
||||||
|
status: enabled ? i18n("Active") : i18n("Inactive")
|
||||||
|
enabled: ShellSettings.Settings.gamingModeEnabled
|
||||||
|
|
||||||
|
function requestDisable() {
|
||||||
|
confirmDisableDialog.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (ShellSettings.Settings.gamingModeEnabled) {
|
||||||
|
requestDisable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShellSettings.Settings.gamingModeEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: confirmDisableDialog
|
||||||
|
active: false
|
||||||
|
onLoaded: item.open()
|
||||||
|
|
||||||
|
sourceComponent: Kirigami.PromptDialog {
|
||||||
|
id: theConfirmDialog
|
||||||
|
title: i18n("Leave gaming mode?")
|
||||||
|
subtitle: i18n("Your games will keep running in the background.")
|
||||||
|
standardButtons: Kirigami.Dialog.NoButton
|
||||||
|
customFooterActions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18n("Keep Playing")
|
||||||
|
onTriggered: theConfirmDialog.close()
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18n("Leave")
|
||||||
|
onTriggered: {
|
||||||
|
ShellSettings.Settings.gamingModeEnabled = false
|
||||||
|
theConfirmDialog.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
onClosed: confirmDisableDialog.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
quicksettings/gaming/metadata.json
Normal file
18
quicksettings/gaming/metadata.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"KPackageStructure": "KPackage/GenericQML",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Email": "mightymarco4@gmail.com",
|
||||||
|
"Name": "Marco Allegretti"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Description": "Quick setting to toggle gaming mode for Plasma Mobile",
|
||||||
|
"Icon": "input-gaming",
|
||||||
|
"Id": "org.kde.plasma.quicksetting.gaming",
|
||||||
|
"License": "EUPL-1.2",
|
||||||
|
"Name": "Gaming Mode",
|
||||||
|
"Version": "0.1",
|
||||||
|
"Website": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
15
quicksettings/gaminghint/Messages.sh
Normal file
15
quicksettings/gaminghint/Messages.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
# SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
set -e
|
||||||
|
: "${XGETTEXT:?XGETTEXT is not set}"
|
||||||
|
: "${podir:?podir is not set}"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
files=(*.json contents/ui/*.qml)
|
||||||
|
if [[ ${#files[@]} -eq 0 ]]; then
|
||||||
|
echo "Messages.sh: no input files found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$XGETTEXT" "${files[@]}" -o "$podir/plasma_mobile_qt.pot"
|
||||||
19
quicksettings/gaminghint/contents/ui/main.qml
Normal file
19
quicksettings/gaminghint/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
|
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||||
|
|
||||||
|
QS.QuickSetting {
|
||||||
|
text: i18n("Launch Hint")
|
||||||
|
icon: "dialog-information"
|
||||||
|
status: ShellSettings.Settings.gamingDismissHintEnabled ? i18n("On") : i18n("Off")
|
||||||
|
enabled: true
|
||||||
|
available: ShellSettings.Settings.gamingModeEnabled
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
ShellSettings.Settings.gamingDismissHintEnabled = !ShellSettings.Settings.gamingDismissHintEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
quicksettings/gaminghint/metadata.json
Normal file
18
quicksettings/gaminghint/metadata.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"KPackageStructure": "KPackage/GenericQML",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Email": "mightymarco4@gmail.com",
|
||||||
|
"Name": "Marco Allegretti"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Description": "Quick setting to toggle the Game Center dismiss hint banner",
|
||||||
|
"Icon": "dialog-information",
|
||||||
|
"Id": "org.kde.plasma.quicksetting.gaminghint",
|
||||||
|
"License": "EUPL-1.2",
|
||||||
|
"Name": "Game Center Hint",
|
||||||
|
"Version": "0.1",
|
||||||
|
"Website": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
SPDX-FileCopyrightText: 2026 Marco Allegretti.
|
SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
SPDX-License-Identifier: EUPL-1.2
|
SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue