Compare commits

..

No commits in common. "ddc2e4cb81365a1ee99d14bb5becedbc79334600" and "d4da69b2f2564eb269c9c544cfb8309fb0f03131" have entirely different histories.

60 changed files with 138 additions and 6692 deletions

1
.gitignore vendored
View file

@ -20,5 +20,4 @@ build
*.kdev4
/build*
.prefix/
.preview-config/
preview.sh

View file

@ -112,7 +112,6 @@ set_package_properties(PlasmaWaylandProtocols PROPERTIES
find_package(LibKWorkspace CONFIG REQUIRED)
find_package(Libudev REQUIRED)
find_package(SDL3 REQUIRED)
find_package(KWinDBusInterface)
set_package_properties(KWinDBusInterface PROPERTIES DESCRIPTION "KWin DBus interface"
@ -141,8 +140,6 @@ plasma_install_package(lookandfeel org.kde.breeze.mobile look-and-feel lookandfe
plasma_install_package(shell org.kde.plasma.mobileshell shells)
add_subdirectory(bin)
add_subdirectory(color-schemes)
add_subdirectory(icons)
add_subdirectory(components)
add_subdirectory(containments)
add_subdirectory(devices)

View file

@ -1,7 +0,0 @@
{
"folders": [
{
"path": "."
}
]
}

View file

@ -1,8 +0,0 @@
# SPDX-FileCopyrightText: 2026 Shift contributors
# SPDX-License-Identifier: GPL-2.0-or-later
install(FILES
ShiftDark.colors
ShiftLight.colors
DESTINATION ${KDE_INSTALL_DATADIR}/color-schemes
)

View file

@ -1,156 +0,0 @@
# SPDX-FileCopyrightText: Andrew Lake <jamboarder@gmail.com>
# SPDX-FileCopyrightText: Marco Martin <notmart@gmail.com>
# SPDX-FileCopyrightText: Nate Graham <nate@kde.org>
# SPDX-FileCopyrightText: Noah Davis <noahadvs@gmail.com>
# SPDX-FileCopyrightText: Neal Gompa <ngompa@kde.org>
# SPDX-FileCopyrightText: David Redondo <kde@david-redondo.de>
# SPDX-FileCopyrightText: Thomas Duckworth <tduck973564@gmail.com>
# SPDX-License-Identifier: LGPL-2.0-or-later
[ColorEffects:Disabled]
Color=56,56,56
ColorAmount=0
ColorEffect=0
ContrastAmount=0.65
ContrastEffect=1
IntensityAmount=0.1
IntensityEffect=2
[ColorEffects:Inactive]
ChangeSelectionColor=true
Color=112,111,110
ColorAmount=0.025
ColorEffect=2
ContrastAmount=0.1
ContrastEffect=2
Enable=false
IntensityAmount=0
IntensityEffect=0
[Colors:Button]
BackgroundAlternate=16,82,74
BackgroundNormal=41,44,48
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Complementary]
BackgroundAlternate=16,82,74
BackgroundNormal=32,35,38
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Header]
BackgroundAlternate=32,35,38
BackgroundNormal=41,44,48
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Header][Inactive]
BackgroundAlternate=41,44,48
BackgroundNormal=32,35,38
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Selection]
BackgroundAlternate=16,82,74
BackgroundNormal=46,184,168
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=252,252,252
ForegroundInactive=161,169,177
ForegroundLink=253,188,75
ForegroundNegative=176,55,69
ForegroundNeutral=198,92,0
ForegroundNormal=252,252,252
ForegroundPositive=23,104,57
ForegroundVisited=155,89,182
[Colors:Tooltip]
BackgroundAlternate=32,35,38
BackgroundNormal=41,44,48
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:View]
BackgroundAlternate=29,31,34
BackgroundNormal=20,22,24
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Window]
BackgroundAlternate=41,44,48
BackgroundNormal=32,35,38
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[General]
ColorScheme=ShiftDark
Name=Shift Dark
shadeSortColumn=true
[KDE]
contrast=4
[WM]
activeBackground=39,44,49
activeBlend=252,252,252
activeForeground=252,252,252
inactiveBackground=32,36,40
inactiveBlend=161,169,177
inactiveForeground=161,169,177

View file

@ -1,155 +0,0 @@
# SPDX-FileCopyrightText: Andrew Lake <jamboarder@gmail.com>
# SPDX-FileCopyrightText: Marco Martin <notmart@gmail.com>
# SPDX-FileCopyrightText: Nate Graham <nate@kde.org>
# SPDX-FileCopyrightText: Noah Davis <noahadvs@gmail.com>
# SPDX-FileCopyrightText: Neal Gompa <ngompa@kde.org>
# SPDX-FileCopyrightText: David Redondo <kde@david-redondo.de>
# SPDX-License-Identifier: LGPL-2.0-or-later
[ColorEffects:Disabled]
Color=56,56,56
ColorAmount=0
ColorEffect=0
ContrastAmount=0.65
ContrastEffect=1
IntensityAmount=0.1
IntensityEffect=2
[ColorEffects:Inactive]
ChangeSelectionColor=true
Color=112,111,110
ColorAmount=0.025
ColorEffect=2
ContrastAmount=0.1
ContrastEffect=2
Enable=false
IntensityAmount=0
IntensityEffect=0
[Colors:Button]
BackgroundAlternate=163,218,212
BackgroundNormal=252,252,252
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=112,125,138
ForegroundLink=15,105,96
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=35,38,41
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Complementary]
BackgroundAlternate=27,30,32
BackgroundNormal=42,46,50
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=161,169,177
ForegroundLink=38,196,180
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=252,252,252
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Header]
BackgroundAlternate=239,240,241
BackgroundNormal=222,224,226
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=112,125,138
ForegroundLink=15,105,96
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=35,38,41
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Header][Inactive]
BackgroundAlternate=227,229,231
BackgroundNormal=239,240,241
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=112,125,138
ForegroundLink=15,105,96
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=35,38,41
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Selection]
BackgroundAlternate=163,218,212
BackgroundNormal=46,184,168
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=255,255,255
ForegroundInactive=112,125,138
ForegroundLink=253,188,75
ForegroundNegative=176,55,69
ForegroundNeutral=198,92,0
ForegroundNormal=255,255,255
ForegroundPositive=23,104,57
ForegroundVisited=155,89,182
[Colors:Tooltip]
BackgroundAlternate=239,240,241
BackgroundNormal=247,247,247
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=112,125,138
ForegroundLink=15,105,96
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=35,38,41
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:View]
BackgroundAlternate=247,247,247
BackgroundNormal=255,255,255
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=112,125,138
ForegroundLink=15,105,96
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=35,38,41
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Window]
BackgroundAlternate=227,229,231
BackgroundNormal=239,240,241
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
ForegroundInactive=112,125,138
ForegroundLink=15,105,96
ForegroundNegative=218,68,83
ForegroundNeutral=246,116,0
ForegroundNormal=35,38,41
ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[General]
ColorScheme=ShiftLight
Name=Shift Light
shadeSortColumn=true
[KDE]
contrast=4
[WM]
activeBackground=227,229,231
activeBlend=227,229,231
activeForeground=35,38,41
inactiveBackground=239,240,241
inactiveBlend=239,240,241
inactiveForeground=112,125,138

View file

@ -14,4 +14,3 @@ add_subdirectory(wallpaperimageplugin)
add_subdirectory(dpmsplugin)
add_subdirectory(screenbrightnessplugin)
add_subdirectory(raiselockscreenplugin)
add_subdirectory(gamingshellplugin)

View file

@ -1,31 +0,0 @@
# 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)

File diff suppressed because it is too large Load diff

View file

@ -1,146 +0,0 @@
// 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;
};

View file

@ -1,64 +0,0 @@
// 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();
}
}

View file

@ -1,46 +0,0 @@
// 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;
};

View file

@ -1,240 +0,0 @@
// 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();
}
}

View file

@ -1,63 +0,0 @@
// 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;
};

View file

@ -1,314 +0,0 @@
// 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;
}

View file

@ -1,106 +0,0 @@
// 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;
};

View file

@ -1,127 +0,0 @@
// 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();
}
}

View file

@ -1,44 +0,0 @@
// 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;
};

View file

@ -86,15 +86,13 @@ QQuickItem *AppletHost::fullRepresentationFor(const QString &pluginId)
m_containment->addApplet(applet);
auto *item = PlasmaQuick::AppletQuickItem::itemForApplet(applet);
m_items.insert(pluginId, item);
if (!item) {
qWarning() << "AppletHost: no AppletQuickItem for" << pluginId;
applet->deleteLater();
return nullptr;
}
m_items.insert(pluginId, item);
item->setPreloadFullRepresentation(true);
auto *fullRepItem = item->fullRepresentationItem();

View file

@ -165,7 +165,6 @@ Item {
shrinkSize: 0
onClicked: {
if (!root.taskModel) return;
root.taskModel.requestActivate(root.taskModel.makeModelIndex(taskDelegate.index));
}
@ -181,27 +180,18 @@ Item {
Controls.MenuItem {
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"
onTriggered: {
if (!root.taskModel) return;
root.taskModel.requestToggleMinimized(root.taskModel.makeModelIndex(taskDelegate.index))
}
onTriggered: root.taskModel.requestToggleMinimized(root.taskModel.makeModelIndex(taskDelegate.index))
}
Controls.MenuItem {
text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize")
icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize"
onTriggered: {
if (!root.taskModel) return;
root.taskModel.requestToggleMaximized(root.taskModel.makeModelIndex(taskDelegate.index))
}
onTriggered: root.taskModel.requestToggleMaximized(root.taskModel.makeModelIndex(taskDelegate.index))
}
Controls.MenuSeparator {}
Controls.MenuItem {
text: i18n("Close")
icon.name: "window-close"
onTriggered: {
if (!root.taskModel) return;
root.taskModel.requestClose(root.taskModel.makeModelIndex(taskDelegate.index))
}
onTriggered: root.taskModel.requestClose(root.taskModel.makeModelIndex(taskDelegate.index))
}
}

View file

@ -212,7 +212,7 @@ Item {
}
}
// Down-arrow hint for convergence mode
// Down-arrow hover hint for convergence mode
Kirigami.Icon {
z: 2
source: 'arrow-down'
@ -224,7 +224,7 @@ Item {
anchors.bottomMargin: Kirigami.Units.smallSpacing
visible: ShellSettings.Settings.convergenceModeEnabled
opacity: statusBarHover.hovered ? 0.6 : 0.2
opacity: statusBarHover.hovered ? 0.8 : 0
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration }

View file

@ -45,8 +45,6 @@ QList<QString> QuickSettingsConfig::enabledQuickSettings() const
QStringLiteral("org.kde.plasma.quicksetting.screenshot"),
QStringLiteral("org.kde.plasma.quicksetting.powermenu"),
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.keyboardtoggle"),
QStringLiteral("org.kde.plasma.quicksetting.hotspot")});

View file

@ -44,8 +44,6 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
Q_EMIT actionDrawerTopRightModeChanged();
Q_EMIT convergenceModeEnabledChanged();
Q_EMIT autoHidePanelsEnabledChanged();
Q_EMIT gamingModeEnabledChanged();
Q_EMIT gamingDismissHintEnabledChanged();
Q_EMIT allowLogoutChanged();
}
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
@ -250,32 +248,6 @@ void MobileShellSettings::setAutoHidePanelsEnabled(bool enabled)
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()
{
// Do not update panels when not in Plasma Mobile

View file

@ -52,10 +52,6 @@ class MobileShellSettings : public QObject
// Auto Hide Panels
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
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
@ -268,12 +264,6 @@ public:
*/
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.
*/
@ -320,8 +310,6 @@ Q_SIGNALS:
void quickSettingsColumnsChanged();
void convergenceModeEnabledChanged();
void autoHidePanelsEnabledChanged();
void gamingModeEnabledChanged();
void gamingDismissHintEnabledChanged();
void allowLogoutChanged();
void lockscreenLeftButtonActionChanged();
void lockscreenRightButtonActionChanged();

View file

@ -35,17 +35,6 @@ void WaydroidApplicationListModel::initializeApplications(const QList<QDBusObjec
endResetModel();
}
void WaydroidApplicationListModel::clearApplications()
{
if (m_applications.isEmpty()) {
return;
}
beginResetModel();
m_applications.clear();
endResetModel();
}
void WaydroidApplicationListModel::addApplication(const QDBusObjectPath &objectPath)
{
beginInsertRows({}, m_applications.size(), m_applications.size());

View file

@ -33,7 +33,6 @@ public:
QHash<int, QByteArray> roleNames() const override;
void initializeApplications(const QList<QDBusObjectPath> &applicationObjectPaths);
void clearApplications();
public Q_SLOTS:
void addApplication(const QDBusObjectPath &objectPath);

View file

@ -6,38 +6,24 @@
#include "waydroiddbusclient.h"
#include <KConfigGroup>
#include <QClipboard>
#include <QCoroDBusPendingReply>
#include <QDBusMessage>
#include <QGuiApplication>
#include <QTimer>
using namespace Qt::StringLiterals;
static const QString s_waydroidGamingGroup = QStringLiteral("WaydroidGaming");
static const QString s_gameShellPackagesKey = QStringLiteral("gameShellPackages");
WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
: QObject{parent}
, 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_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
if (QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.plasmashell"_s)) {
checkWaydroidObject();
m_connected = true;
if (m_interface->isValid()) {
connectSignals();
}
}
connect(m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this](const QString &service, const QString &oldOwner, const QString &newOwner) {
@ -45,11 +31,12 @@ WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
if (newOwner.isEmpty()) {
// Service stopped
m_connected = false;
m_connectionCheckPending = false;
resetState();
} else if (oldOwner.isEmpty()) {
// Service started
checkWaydroidObject();
m_connected = true;
if (m_interface->isValid()) {
connectSignals();
}
}
}
});
@ -57,9 +44,6 @@ WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
void WaydroidDBusClient::connectSignals()
{
if (!m_signalsConnected) {
m_signalsConnected = true;
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::statusChanged, this, &WaydroidDBusClient::updateStatus);
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::downloadStatusChanged, this, [this](double downloaded, double total, double speed) {
Q_EMIT downloadStatusChanged(downloaded, total, speed);
@ -71,8 +55,6 @@ void WaydroidDBusClient::connectSignals()
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::multiWindowsChanged, this, &WaydroidDBusClient::updateMultiWindows);
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::suspendChanged, this, &WaydroidDBusClient::updateSuspend);
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) {
Q_EMIT actionFinished(message);
});
@ -82,7 +64,6 @@ void WaydroidDBusClient::connectSignals()
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::errorOccurred, this, [this](const QString title, const QString message) {
Q_EMIT errorOccurred(title, message);
});
}
initializeApplicationListModel();
updateStatus();
@ -93,118 +74,6 @@ void WaydroidDBusClient::connectSignals()
updateMultiWindows();
updateSuspend();
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()
@ -216,7 +85,6 @@ void WaydroidDBusClient::initializeApplicationListModel()
QDBusPendingReply<QList<QDBusObjectPath>> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch applications:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -265,10 +133,6 @@ WaydroidApplicationListModel *WaydroidDBusClient::applicationListModel() const
QCoro::Task<void> WaydroidDBusClient::setMultiWindowsTask(const bool multiWindows)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setMultiWindows(multiWindows);
}
@ -284,10 +148,6 @@ bool WaydroidDBusClient::multiWindows() const
QCoro::Task<void> WaydroidDBusClient::setSuspendTask(const bool suspend)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setSuspend(suspend);
}
@ -303,10 +163,6 @@ bool WaydroidDBusClient::suspend() const
QCoro::Task<void> WaydroidDBusClient::setUeventTask(const bool uevent)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setUevent(uevent);
}
@ -317,10 +173,6 @@ QCoro::QmlTask WaydroidDBusClient::setUevent(const bool uevent)
QCoro::Task<void> WaydroidDBusClient::refreshSessionInfoTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->refreshSessionInfo();
}
@ -331,10 +183,6 @@ QCoro::QmlTask WaydroidDBusClient::refreshSessionInfo()
QCoro::Task<void> WaydroidDBusClient::refreshAndroidIdTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->refreshAndroidId();
}
@ -345,10 +193,6 @@ QCoro::QmlTask WaydroidDBusClient::refreshAndroidId()
QCoro::Task<void> WaydroidDBusClient::refreshApplicationsTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->refreshApplications();
}
@ -357,87 +201,13 @@ QCoro::QmlTask WaydroidDBusClient::refreshApplications()
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
{
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)
{
if (!m_connected) {
co_return;
}
co_await m_interface->initialize(systemType, romType, forced);
}
@ -448,10 +218,6 @@ QCoro::QmlTask WaydroidDBusClient::initialize(const SystemType systemType, const
QCoro::Task<void> WaydroidDBusClient::startSessionTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->startSession();
}
@ -462,10 +228,6 @@ QCoro::QmlTask WaydroidDBusClient::startSession()
QCoro::Task<void> WaydroidDBusClient::stopSessionTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->stopSession();
}
@ -476,10 +238,6 @@ QCoro::QmlTask WaydroidDBusClient::stopSession()
QCoro::Task<void> WaydroidDBusClient::resetWaydroidTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->resetWaydroid();
}
@ -490,10 +248,6 @@ QCoro::QmlTask WaydroidDBusClient::resetWaydroid()
QCoro::Task<void> WaydroidDBusClient::installApkTask(const QString apkFile)
{
if (!m_connected) {
co_return;
}
co_await m_interface->installApk(apkFile);
}
@ -502,26 +256,8 @@ QCoro::QmlTask WaydroidDBusClient::installApk(const QString 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)
{
if (!m_connected) {
co_return;
}
co_await m_interface->deleteApplication(appId);
}
@ -530,20 +266,6 @@ QCoro::QmlTask WaydroidDBusClient::deleteApplication(const QString 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()
{
auto reply = m_interface->status();
@ -553,7 +275,6 @@ void WaydroidDBusClient::updateStatus()
QDBusPendingReply<int> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch status:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -578,7 +299,6 @@ void WaydroidDBusClient::updateSessionStatus()
QDBusPendingReply<int> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch sessionStatus:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -603,7 +323,6 @@ void WaydroidDBusClient::updateSystemType()
QDBusPendingReply<int> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch systemType:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -628,7 +347,6 @@ void WaydroidDBusClient::updateIpAddress()
QDBusPendingReply<QString> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch ipAddress:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -653,7 +371,6 @@ void WaydroidDBusClient::updateAndroidId()
QDBusPendingReply<QString> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch androidId:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -678,7 +395,6 @@ void WaydroidDBusClient::updateMultiWindows()
QDBusPendingReply<bool> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch multiWindows:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -703,7 +419,6 @@ void WaydroidDBusClient::updateSuspend()
QDBusPendingReply<bool> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch suspend:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -728,7 +443,6 @@ void WaydroidDBusClient::updateUevent()
QDBusPendingReply<bool> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch uevent:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -744,72 +458,7 @@ 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)
{
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();
}

View file

@ -10,12 +10,8 @@
#include "waydroidapplicationlistmodel.h"
#include "waydroiddbusobject.h"
#include <KConfigWatcher>
#include <KSharedConfig>
#include <QCoroCore>
#include <QCoroQmlTask>
#include <QDBusPendingCallWatcher>
#include <QDBusServiceWatcher>
#include <QObject>
#include <QString>
@ -36,9 +32,6 @@ class WaydroidDBusClient : public QObject
Q_PROPERTY(bool multiWindows READ multiWindows WRITE setMultiWindows NOTIFY multiWindowsChanged)
Q_PROPERTY(bool suspend READ suspend WRITE setSuspend NOTIFY suspendChanged)
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)
public:
@ -105,25 +98,16 @@ public:
QCoro::QmlTask setSuspend(const bool suspend);
[[nodiscard]] bool uevent() const;
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 startSession();
Q_INVOKABLE QCoro::QmlTask stopSession();
Q_INVOKABLE QCoro::QmlTask resetWaydroid();
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 refreshSupportsInfo();
Q_INVOKABLE QCoro::QmlTask refreshSessionInfo();
Q_INVOKABLE QCoro::QmlTask refreshAndroidId();
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);
@ -138,9 +122,6 @@ Q_SIGNALS:
void multiWindowsChanged();
void suspendChanged();
void ueventChanged();
void fakeTouchChanged();
void fakeWifiChanged();
void gameShellPackagesChanged();
void actionFinished(const QString message);
void actionFailed(const QString message);
@ -155,14 +136,12 @@ private Q_SLOTS:
void updateMultiWindows();
void updateSuspend();
void updateUevent();
void updateFakeTouch();
void updateFakeWifi();
private:
OrgKdePlasmashellWaydroidInterface *m_interface;
QDBusServiceWatcher *m_watcher;
Status m_status{NotSupported};
Status m_status{NotInitialized};
SessionStatus m_sessionStatus{SessionStopped};
SystemType m_systemType{UnknownSystemType};
QString m_ipAddress{""};
@ -173,40 +152,22 @@ private:
bool m_multiWindows{false};
bool m_suspend{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_signalsConnected{false};
bool m_connectionCheckPending{false};
void connectSignals();
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> startSessionTask();
QCoro::Task<void> stopSessionTask();
QCoro::Task<void> resetWaydroidTask();
QCoro::Task<void> installApkTask(const QString apkFile);
QCoro::Task<void> launchApplicationTask(const QString appId);
QCoro::Task<void> deleteApplicationTask(const QString appId);
QCoro::Task<void> refreshSupportsInfoTask();
QCoro::Task<void> setMultiWindowsTask(const bool multiWindows);
QCoro::Task<void> setSuspendTask(const bool suspend);
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> refreshAndroidIdTask();
QCoro::Task<void> refreshApplicationsTask();
void reloadGameShellPackages();
};

View file

@ -31,8 +31,6 @@ using namespace Qt::StringLiterals;
#define MULTI_WINDOWS_PROP_KEY "persist.waydroid.multi_windows"
#define SUSPEND_PROP_KEY "persist.waydroid.suspend"
#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 ipAddressRegExp(u"IP address:\\s*(\\d+\\.\\d+\\.\\d+\\.\\d+)"_s);
@ -181,12 +179,10 @@ void WaydroidDBusObject::stopSession()
connect(process, &QProcess::finished, this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) {
Q_UNUSED(exitStatus);
const QByteArray errorLog = process->readAllStandardError();
process->deleteLater();
if (exitCode != 0) {
Q_EMIT errorOccurred(i18n("Failed to stop the Waydroid session."), QString::fromUtf8(errorLog));
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << errorLog;
if (exitCode == 0) {
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << process->readAllStandardError();
return;
}
@ -254,27 +250,6 @@ void WaydroidDBusObject::installApk(const QString apkFile)
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)
{
const QStringList arguments{u"app"_s, u"remove"_s, appId};
@ -400,52 +375,6 @@ void WaydroidDBusObject::setUevent(const bool 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> paths;
@ -584,7 +513,6 @@ void WaydroidDBusObject::refreshAndroidId()
}
} else {
m_androidId = "";
Q_EMIT actionFailed(i18n("Failed to fetch Android ID"));
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << executeJob->error();
}
@ -611,12 +539,6 @@ QCoro::Task<void> WaydroidDBusObject::refreshPropsInfo()
const QString ueventPropValue = co_await fetchPropValue(UEVENT_PROP_KEY, "false");
m_uevent = ueventPropValue == "true";
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)

View file

@ -96,8 +96,6 @@ Q_SIGNALS:
Q_SCRIPTABLE void multiWindowsChanged();
Q_SCRIPTABLE void suspendChanged();
Q_SCRIPTABLE void ueventChanged();
Q_SCRIPTABLE void fakeTouchChanged();
Q_SCRIPTABLE void fakeWifiChanged();
Q_SCRIPTABLE void applicationAdded(QDBusObjectPath path);
Q_SCRIPTABLE void applicationRemoved(QDBusObjectPath path);
@ -121,10 +119,6 @@ public Q_SLOTS:
Q_SCRIPTABLE void setSuspend(const bool suspend);
Q_SCRIPTABLE bool uevent() const;
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 void initialize(const int systemType, const int romType, const bool forced = false);
@ -132,9 +126,7 @@ public Q_SLOTS:
Q_SCRIPTABLE void stopSession();
Q_SCRIPTABLE void resetWaydroid();
Q_SCRIPTABLE void installApk(const QString apkFile);
Q_SCRIPTABLE void launchApplication(const QString appId);
Q_SCRIPTABLE void deleteApplication(const QString appId);
Q_SCRIPTABLE void refreshSupportsInfo();
Q_SCRIPTABLE void refreshSessionInfo();
Q_SCRIPTABLE void refreshAndroidId();
Q_SCRIPTABLE void refreshApplications();
@ -151,9 +143,8 @@ private:
bool m_multiWindows{false};
bool m_suspend{false};
bool m_uevent{false};
QString m_fakeTouch;
QString m_fakeWifi;
void refreshSupportsInfo();
void refreshInstallationInfo();
QCoro::Task<void> refreshPropsInfo();

View file

@ -76,14 +76,6 @@ ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
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
Qt::Gui
Qt::Qml

View file

@ -11,7 +11,6 @@ import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import "./delegate"
@ -48,11 +47,8 @@ MobileShell.GridView {
// HACK: the first swipe from the top of the app drawer is done from HomeScreenState, not the flickable
// due to issues with Flickable getting its swipe stolen by SwipeArea
// In convergence mode the drawer is a popup opened by a button no swipe-open gesture,
// so the grid must always be scrollable via mouse wheel/drag.
interactive: ShellSettings.Settings.convergenceModeEnabled
|| ((dragging || !atYBeginning) // allow us to drag to the top
&& folio.HomeScreenState.swipeState !== Folio.HomeScreenState.SwipingAppDrawerGrid)
interactive: (dragging || !atYBeginning) // allow us to drag to the top
&& folio.HomeScreenState.swipeState !== Folio.HomeScreenState.SwipingAppDrawerGrid
Connections {
target: folio.HomeScreenState

View file

@ -15,9 +15,8 @@ Item {
id: root
property Folio.HomeScreen folio
// Do not override the colorset: in mobile mode we inherit Complementary
// from the containment (wallpaper context, white text); in convergence mode
// the drawerOverlay Window gives us Window context (system-adaptive).
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
function addSearchText(text: string) {
searchField.text += text;
@ -58,21 +57,22 @@ Item {
background: Rectangle {
radius: Kirigami.Units.cornerRadius
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g,
Kirigami.Theme.textColor.b,
(searchField.hovered || searchField.focus) ? 0.2 : 0.1)
color: Qt.rgba(255, 255, 255, (searchField.hovered || searchField.focus) ? 0.2 : 0.1)
Behavior on color { ColorAnimation {} }
}
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
topPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
Layout.fillWidth: true
horizontalAlignment: QQC2.TextField.AlignHCenter
placeholderText: i18nc("@info:placeholder", "Search applications…")
placeholderTextColor: Kirigami.Theme.disabledTextColor
color: Kirigami.Theme.textColor
placeholderTextColor: Qt.rgba(255, 255, 255, 0.8)
color: 'white'
font.weight: Font.Bold

View file

@ -125,41 +125,20 @@ MouseArea {
Rectangle {
id: homeButton
visible: root.convergenceMode
activeFocusOnTab: root.convergenceMode
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.navButtonWidth
color: homeMouseArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: (homeMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
? Qt.rgba(1, 1, 1, 0.2)
: (homeMouseArea.containsMouse ? Qt.rgba(1, 1, 1, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius
Accessible.role: Accessible.Button
Accessible.name: i18n("Home")
Accessible.onPressAction: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onReturnPressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onEnterPressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onSpacePressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onRightPressed: {
let first = repeater.itemAt(0)
if (first) { first.keyboardFocus(); return }
let firstTask = taskRepeater.itemAt(0)
if (firstTask) { firstTask.forceActiveFocus(); return }
overviewButton.forceActiveFocus()
}
KeyboardHighlight {
anchors.fill: parent
visible: homeButton.activeFocus
}
Kirigami.Icon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75
height: width
source: "start-here-shift"
source: "start-here-kde"
active: homeMouseArea.containsMouse
}
@ -176,36 +155,15 @@ MouseArea {
Rectangle {
id: overviewButton
visible: root.convergenceMode
activeFocusOnTab: root.convergenceMode
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.navButtonWidth
color: overviewMouseArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: (overviewMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
? Qt.rgba(1, 1, 1, 0.2)
: (overviewMouseArea.containsMouse ? Qt.rgba(1, 1, 1, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius
Accessible.role: Accessible.Button
Accessible.name: i18n("Overview")
Accessible.onPressAction: root.folio.triggerOverview()
Keys.onReturnPressed: root.folio.triggerOverview()
Keys.onEnterPressed: root.folio.triggerOverview()
Keys.onSpacePressed: root.folio.triggerOverview()
Keys.onLeftPressed: {
let lastTask = taskRepeater.itemAt(taskRepeater.count - 1)
if (lastTask) { lastTask.forceActiveFocus(); return }
let lastFav = repeater.itemAt(repeater.count - 1)
if (lastFav) { lastFav.keyboardFocus(); return }
homeButton.forceActiveFocus()
}
KeyboardHighlight {
anchors.fill: parent
visible: overviewButton.activeFocus
}
Kirigami.Icon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75
@ -362,12 +320,9 @@ MouseArea {
break;
case Qt.Key_Left:
if (isLocationBottom) {
let prevDelegate = repeater.itemAt(delegate.index - 1);
if (prevDelegate) {
prevDelegate.keyboardFocus();
event.accepted = true;
} else if (root.convergenceMode) {
homeButton.forceActiveFocus();
let nextDelegate = repeater.itemAt(delegate.index - 1);
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
}
}
@ -378,14 +333,6 @@ MouseArea {
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
} else if (root.convergenceMode) {
let firstTask = taskRepeater.itemAt(0);
if (firstTask) {
firstTask.forceActiveFocus();
} else {
overviewButton.forceActiveFocus();
}
event.accepted = true;
}
}
break;
@ -652,7 +599,7 @@ MouseArea {
id: thumbnailShowTimer
interval: Kirigami.Units.toolTipDelay
onTriggered: {
thumbnailPopup.showing = true
thumbnailPopup.visible = true
}
}
@ -660,7 +607,7 @@ MouseArea {
id: thumbnailHideTimer
interval: 300
onTriggered: {
thumbnailPopup.showing = false
thumbnailPopup.visible = false
root.hoveredTaskIndex = -1
}
}
@ -673,21 +620,10 @@ MouseArea {
property var windowIds: []
property bool isGroup: false
property bool popupHovered: false
property bool showing: false
function open() { showing = true }
function close() { showing = false }
readonly property bool opened: showing
visible: showing || fadeAnim.running
opacity: showing ? 1 : 0
Behavior on opacity {
NumberAnimation {
id: fadeAnim
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
}
}
function open() { visible = true }
function close() { visible = false }
readonly property bool opened: visible
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus
color: "transparent"
@ -706,24 +642,16 @@ MouseArea {
x: {
if (!targetDelegate) return 0
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
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))
return Math.max(0, Math.min(Screen.width - width, delegateGlobal.x + (targetDelegate.width - width) / 2))
}
y: {
if (!targetDelegate) return 0
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
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))
return Math.max(0, Math.min(Screen.height - height, delegateGlobal.y - height - Kirigami.Units.smallSpacing))
}
onShowingChanged: {
if (!showing && !fadeAnim.running) {
onVisibleChanged: {
if (!visible) {
windowIds = []
targetDelegate = null
taskIndex = -1
@ -731,18 +659,6 @@ MouseArea {
}
}
Connections {
target: fadeAnim
function onRunningChanged() {
if (!fadeAnim.running && !thumbnailPopup.showing) {
thumbnailPopup.windowIds = []
thumbnailPopup.targetDelegate = null
thumbnailPopup.taskIndex = -1
thumbnailPopup.isGroup = false
}
}
}
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
@ -913,49 +829,9 @@ MouseArea {
required property int index
required property var model
activeFocusOnTab: root.convergenceMode
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model)
Accessible.role: Accessible.Button
Accessible.name: taskDelegate.model.display || ""
Accessible.onPressAction: taskDelegate.activateTask()
function activateTask() {
var winIds = taskDelegate.model.WinIdList
if (winIds && winIds.length > 1) {
if (thumbnailPopup.opened && thumbnailPopup.taskIndex === taskDelegate.index) {
thumbnailPopup.close()
} else {
thumbnailPopup.targetDelegate = taskDelegate
thumbnailPopup.taskIndex = taskDelegate.index
thumbnailPopup.windowIds = winIds
thumbnailPopup.isGroup = taskDelegate.model.IsGroupParent === true
thumbnailPopup.open()
}
} else {
thumbnailPopup.close()
tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index))
}
}
Keys.onReturnPressed: taskDelegate.activateTask()
Keys.onEnterPressed: taskDelegate.activateTask()
Keys.onSpacePressed: taskDelegate.activateTask()
Keys.onLeftPressed: {
let prev = taskRepeater.itemAt(taskDelegate.index - 1)
if (prev) { prev.forceActiveFocus(); return }
let lastFav = repeater.itemAt(repeater.count - 1)
if (lastFav) { lastFav.keyboardFocus(); return }
homeButton.forceActiveFocus()
}
Keys.onRightPressed: {
let next = taskRepeater.itemAt(taskDelegate.index + 1)
if (next) { next.forceActiveFocus(); return }
overviewButton.forceActiveFocus()
}
// Position after all favourites
property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2)
Behavior on fromCenterValue {
@ -978,13 +854,8 @@ MouseArea {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: taskMouseArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: (taskMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
}
KeyboardHighlight {
anchors.fill: parent
visible: taskDelegate.activeFocus
? Qt.rgba(1.0, 1.0, 1.0, 0.2)
: (taskMouseArea.containsMouse ? Qt.rgba(1.0, 1.0, 1.0, 0.1) : "transparent")
}
// Task icon
@ -1076,7 +947,22 @@ MouseArea {
thumbnailShowTimer.stop()
taskContextMenu.open();
} else {
taskDelegate.activateTask()
var winIds = taskDelegate.model.WinIdList
if (winIds && winIds.length > 1) {
// Multiple windows: toggle thumbnail popup
if (thumbnailPopup.opened && thumbnailPopup.taskIndex === taskDelegate.index) {
thumbnailPopup.close()
} else {
thumbnailPopup.targetDelegate = taskDelegate
thumbnailPopup.taskIndex = taskDelegate.index
thumbnailPopup.windowIds = winIds
thumbnailPopup.isGroup = taskDelegate.model.IsGroupParent === true
thumbnailPopup.open()
}
} else {
thumbnailPopup.close()
tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index));
}
}
}
onContainsMouseChanged: {

View file

@ -126,7 +126,7 @@ Folio.DelegateTouchArea {
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
color: Qt.rgba(1.0, 1.0, 1.0, 0.1)
visible: ShellSettings.Settings.convergenceModeEnabled && root.hovered
}
}
@ -142,7 +142,7 @@ Folio.DelegateTouchArea {
Layout.rightMargin: -parent.anchors.rightMargin + Kirigami.Units.smallSpacing
text: root.name
color: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Theme.textColor : "white"
color: "white"
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,216 +0,0 @@
// 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
}
}
}
}

View file

@ -1,692 +0,0 @@
// 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 }
}
}
}
}

View file

@ -1,208 +0,0 @@
// 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()
}
}
}
}
}
}

View file

@ -16,7 +16,6 @@ import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
import org.kde.layershell 1.0 as LayerShell
import org.kde.plasma.private.sessions 2.0
@ -26,84 +25,13 @@ import org.kde.kirigamiaddons.components as KirigamiAddonsComponents
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import "./gaming"
import "./private"
ContainmentItem {
id: root
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: {
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
folio.FolioSettings.load();
folio.FavouritesModel.load();
folio.PageListModel.load();
@ -143,18 +71,6 @@ ContainmentItem {
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
Connections {
target: WindowPlugin.WindowUtil
@ -173,12 +89,6 @@ ContainmentItem {
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) {
// Convergence: toggle the app drawer as a layer-shell overlay
// without disturbing open windows.
@ -276,7 +186,7 @@ ContainmentItem {
// task panel containment; this window only provides the visible dock.
Window {
id: dockOverlay
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
visible: ShellSettings.Settings.convergenceModeEnabled
color: "transparent"
width: Screen.width
height: Kirigami.Units.gridUnit * 3
@ -353,9 +263,7 @@ ContainmentItem {
Rectangle {
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Kirigami.Theme.backgroundColor
color: Qt.rgba(0, 0, 0, 0.5)
transform: Translate { y: dockOverlay.dockOffset }
}
@ -366,11 +274,6 @@ ContainmentItem {
maskManager: root.maskManager
homeScreen: folioHomeScreen
transform: Translate { y: dockOverlay.dockOffset }
// Dock is an opaque panel use Window colorset so all content
// (labels, hover highlights, icon tints) follows the system theme
// instead of the containment's Complementary wallpaper context.
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
}
@ -381,7 +284,6 @@ ContainmentItem {
Window {
id: drawerOverlay
visible: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& folio.HomeScreenState.appDrawerOpenProgress > 0
color: "transparent"
width: Screen.width
@ -741,74 +643,6 @@ 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 {
id: homeScreen
anchors.fill: parent

View file

@ -83,12 +83,6 @@ Item {
return true;
}
// In convergence mode the status bar behaves like a desktop panel:
// always opaque so it matches the dock and doesn't let the wallpaper bleed through.
if (ShellSettings.Settings.convergenceModeEnabled) {
return false;
}
return !containmentItem.showingApp && !containmentItem.fullscreen;
}
forcedComplementary: {
@ -137,7 +131,6 @@ Item {
context: Qt.ApplicationShortcut
onActivated: {
if (drawer.actionDrawer.intendedToBeVisible) {
drawer.actionDrawer.intendedToBeVisible = false;
drawer.actionDrawer.close();
} else {
drawer.actionDrawer.openToPinnedMode = false;

View file

@ -34,17 +34,11 @@ ContainmentItem {
// Whether the startup feedback is showing
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)
readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback
// Whether the currently showing app is in "fullscreen"
readonly property bool fullscreen: {
if (gamingMode) {
return true;
}
// In convergence mode the status bar is always visible, like a desktop panel.
if (ShellSettings.Settings.convergenceModeEnabled) {
return false;
@ -75,7 +69,7 @@ ContainmentItem {
}
}
readonly property real panelHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight
readonly property real panelHeight: MobileShell.Constants.topPanelHeight
onPanelHeightChanged: setWindowProperties()
function setWindowProperties() {
@ -129,11 +123,6 @@ ContainmentItem {
function onConvergenceModeEnabledChanged() {
root.setWindowProperties();
}
function onGamingModeEnabledChanged() {
root.setWindowProperties();
MobileShellState.ShellDBusClient.panelState = ShellSettings.Settings.gamingModeEnabled ? "hidden" : (fullscreen ? "hidden" : "default");
}
}
Component.onCompleted: {
@ -147,23 +136,22 @@ ContainmentItem {
// MaximizeArea by the panel height.
Window {
id: topBarSpaceReserver
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
visible: ShellSettings.Settings.convergenceModeEnabled
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
height: Math.max(1, root.panelHeight)
height: root.panelHeight
width: 1
LayerShell.Window.scope: "topbar-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight)
LayerShell.Window.exclusionZone: root.panelHeight
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}
// Visual panel component
StatusPanel {
id: statusPanel
visible: !ShellSettings.Settings.gamingModeEnabled
anchors.fill: parent
containmentItem: root
}

View file

@ -88,7 +88,7 @@ MobileShell.NavigationPanel {
id: homeAction
enabled: true
iconSource: "start-here-shift"
iconSource: "start-here-kde"
onTriggered: {
MobileShellState.ShellDBusClient.openHomeScreen();

View file

@ -38,14 +38,12 @@ ContainmentItem {
readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height)
readonly property bool gamingMode: ShellSettings.Settings.gamingModeEnabled
readonly property real navigationPanelHeight: gamingMode ? 0 : MobileShell.Constants.navigationPanelThickness
readonly property real navigationPanelHeight: MobileShell.Constants.navigationPanelThickness
onNavigationPanelHeightChanged: setWindowProperties()
readonly property real intendedWindowThickness: navigationPanelHeight
readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width
readonly property real intendedWindowOffset: (inLandscape && !gamingMode) ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
readonly property real intendedWindowOffset: inLandscape ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
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
@ -138,11 +136,6 @@ ContainmentItem {
function onConvergenceModeEnabledChanged() {
root.setWindowProperties();
}
function onGamingModeEnabledChanged() {
root.setWindowProperties();
navigationPanel.offset = ShellSettings.Settings.gamingModeEnabled ? MobileShell.Constants.navigationPanelThickness : 0;
}
}
Component.onCompleted: setWindowProperties();
@ -160,19 +153,18 @@ ContainmentItem {
Window {
id: dockSpaceReserver
visible: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& !(ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow)
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
// height is set by layer-shell anchoring; provide a fallback.
height: Math.max(1, MobileShell.Constants.navigationPanelThickness)
height: Kirigami.Units.gridUnit * 3
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
LayerShell.Window.scope: "dock-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: Math.max(1, MobileShell.Constants.navigationPanelThickness)
LayerShell.Window.exclusionZone: Kirigami.Units.gridUnit * 3
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}
@ -185,9 +177,7 @@ ContainmentItem {
return (windowMaximizedTracker.showingWindow || isCurrentWindowFullscreen) && !showingStartupFeedback
}
readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen
readonly property bool fullscreen: ShellSettings.Settings.gamingModeEnabled
|| isCurrentWindowFullscreen
|| (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)
readonly property bool fullscreen: isCurrentWindowFullscreen || (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
@ -215,7 +205,6 @@ ContainmentItem {
Item {
id: navigationPanel
visible: !ShellSettings.Settings.gamingModeEnabled
anchors.fill: parent
property real offset: 0

View file

@ -1,10 +0,0 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: GPL-2.0-or-later
include(ECMInstallIcons)
ecm_install_icons(
ICONS sc-places-start-here-shift.svg
DESTINATION ${KDE_INSTALL_ICONDIR}
THEME hicolor
)

View file

@ -1,10 +0,0 @@
<svg viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<!-- top-left circle -->
<circle cx="16" cy="16" r="16" fill="#FBFBFB"/>
<circle cx="16" cy="16" r="5.4" fill="#31363B"/>
<!-- bottom-left circle -->
<circle cx="16" cy="80" r="16" fill="#FBFBFB"/>
<circle cx="16" cy="80" r="7.5" fill="#31363B"/>
<!-- right circle -->
<circle cx="80" cy="48" r="16" fill="#FBFBFB"/>
</svg>

Before

Width:  |  Height:  |  Size: 401 B

View file

@ -17,45 +17,6 @@ import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
KCM.SimpleKCM {
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
bottomPadding: Kirigami.Units.largeSpacing
leftPadding: 0
@ -113,7 +74,6 @@ KCM.SimpleKCM {
}
ColumnLayout {
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
spacing: Kirigami.Units.largeSpacing
Kirigami.InlineMessage {
@ -125,11 +85,6 @@ KCM.SimpleKCM {
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 {
Repeater {
model: AIP.WaydroidDBusClient.applicationListModel
@ -140,30 +95,13 @@ KCM.SimpleKCM {
width: ListView.view.width
background: null
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
contentItem: RowLayout {
QQC2.Label {
Layout.fillWidth: true
text: model.name
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 {
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Delete the application")
@ -176,71 +114,6 @@ KCM.SimpleKCM {
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
@ -248,22 +121,4 @@ 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()
}
}
}

View file

@ -15,13 +15,6 @@ import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
ColumnLayout {
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 {
title: i18n("General information")
}
@ -73,64 +66,6 @@ ColumnLayout {
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
@ -205,29 +140,5 @@ ColumnLayout {
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()
}
}
}
}

View file

@ -29,9 +29,7 @@ KCM.SimpleKCM {
}
WaydroidLoader {
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
&& AIP.WaydroidDBusClient.androidId === ""
&& !inlineMessage.visible
visible: AIP.WaydroidDBusClient.androidId === ""
text: i18n("Fetching your Android ID.\nIt can take a few seconds.")
}
@ -55,8 +53,7 @@ KCM.SimpleKCM {
}
ColumnLayout {
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
&& AIP.WaydroidDBusClient.androidId !== ""
visible: AIP.WaydroidDBusClient.androidId !== ""
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent
anchors.leftMargin: Kirigami.Units.largeSpacing
@ -79,22 +76,4 @@ 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()
}
}
}

View file

@ -29,14 +29,14 @@ KCM.SimpleKCM {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Waydroid is unavailable")
text: i18n("Waydroid is not installed")
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
PC3.Button {
text: i18n("Check again")
text: i18n("Check installation")
Layout.alignment: Qt.AlignHCenter
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
}

View file

@ -11,9 +11,9 @@ Loader {
property var currentWindow
// Windows awaiting geometry clamping after un-maximize in convergence
// mode. Using an array so concurrent un-maximizes are not lost.
property var pendingConstrainWindows: []
// Window that needs geometry clamping after un-maximize in convergence
// mode. Set in onMaximizedChanged and consumed by the timer below.
property var pendingConstrainWindow: null
// After a window is un-maximized in convergence mode, the dockSpaceReserver
// LayerShell surface needs one Wayland roundtrip to (re)commit its exclusive
@ -24,17 +24,15 @@ Loader {
id: constrainAfterRestoreTimer
interval: 200
onTriggered: {
const windows = root.pendingConstrainWindows.slice()
root.pendingConstrainWindows = []
for (const window of windows) {
if (!window || window.deleted || !window.normalWindow) continue
if (!ShellSettings.Settings.convergenceModeEnabled) continue
if (ShellSettings.Settings.gamingModeEnabled) continue
const window = root.pendingConstrainWindow
root.pendingConstrainWindow = null
if (!window || window.deleted || !window.normalWindow) return
if (!ShellSettings.Settings.convergenceModeEnabled) return
const output = window.output
const desktop = window.desktops[0]
if (!output) continue
if (!desktop) continue
if (!output) return
if (!desktop) return
const maxRect = KWinComponents.Workspace.clientArea(
KWinComponents.Workspace.MaximizeArea, output, desktop)
@ -50,7 +48,6 @@ Loader {
}
}
}
}
function run(window) {
// HACK: don't maximize xwaylandvideobridge
@ -63,12 +60,6 @@ Loader {
return;
}
if (ShellSettings.Settings.gamingModeEnabled) {
window.noBorder = true;
window.setMaximize(true, true);
return;
}
if (ShellSettings.Settings.convergenceModeEnabled) {
window.noBorder = false;
} else {
@ -100,9 +91,6 @@ Loader {
target: currentWindow
function onFullScreenChanged() {
if (!currentWindow) {
return;
}
currentWindow.interactiveMoveResizeFinished.connect((currentWindow) => {
root.run(currentWindow);
});
@ -110,9 +98,6 @@ Loader {
}
function onMaximizedChanged() {
if (!currentWindow) {
return;
}
if (!currentWindow.maximizable) {
return;
}
@ -125,7 +110,7 @@ Loader {
// zone is re-committed over a Wayland roundtrip.
if (ShellSettings.Settings.convergenceModeEnabled
&& ShellSettings.Settings.autoHidePanelsEnabled) {
root.pendingConstrainWindows.push(currentWindow)
root.pendingConstrainWindow = currentWindow
constrainAfterRestoreTimer.restart()
}
}
@ -143,25 +128,12 @@ 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 {
target: KWinComponents.Workspace
function onWindowAdded(window) {
if (!window) {
return;
}
if (window.normalWindow) {
window.interactiveMoveResizeFinished.connect((window) => {
root.run(window);
@ -171,9 +143,6 @@ Loader {
}
function onWindowActivated(window) {
if (!window) {
return;
}
if (window.normalWindow) {
currentWindow = window;
window.interactiveMoveResizeFinished.connect((window) => {

View file

@ -8,11 +8,11 @@
widgetStyle=Breeze
[kdeglobals][General]
ColorScheme=ShiftDark
Name=Shift Dark
ColorScheme=BreezeLight
Name=Breeze
[kdeglobals][Icons]
Theme=breeze-dark
Theme=breeze
[plasmarc][Theme]
name=default
@ -26,7 +26,6 @@ cursorTheme=breeze_cursors
[kwinrc][org.kde.kdecoration2]
library=org.kde.breeze
theme=Shift Dark
[KSplash]
Theme=org.kde.Breeze

View file

@ -8,8 +8,6 @@ plasma_install_package(bluetooth org.kde.plasma.quicksetting.bluetooth quicksett
plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings)
plasma_install_package(docked org.kde.plasma.quicksetting.docked 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(keyboardtoggle org.kde.plasma.quicksetting.keyboardtoggle quicksettings)
plasma_install_package(kscreenosd org.kde.plasma.quicksetting.kscreenosd quicksettings)

View file

@ -1,16 +0,0 @@
#! /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"

View file

@ -1,58 +0,0 @@
// 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-gamepad"
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
}
}
}

View file

@ -1,18 +0,0 @@
{
"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": ""
}
}

View file

@ -1,15 +0,0 @@
#!/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"

View file

@ -1,19 +0,0 @@
// 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;
}
}

View file

@ -1,18 +0,0 @@
{
"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": ""
}
}

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2026 Marco Allegretti
SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2