mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-07-30 00:04:45 +00:00
Add quick settings panel and game sources
Slide-out panel with brightness, volume, Wi-Fi, Bluetooth, and airplane mode. Fully gamepad-navigable; D-pad and stick input routed to the panel while open. System status bar (clock, battery, connectivity) added to game center header. Lutris games discovered from ~/.local/share/lutris/pga.db, Heroic from ~/.config/heroic/store_cache/ JSON files. Deduplication extended to cover all launcher sources. Desktop entries launched via KIO::ApplicationLauncherJob to expand Exec field codes. Generic commands parsed with KShell::splitArgs. SQLite uses RAII cleanup guard. Clamped SDL axis, rumble, and LED values. Renamed rumble params from frequency to intensity. GamepadManager uses singleton factory. setPlayerIndex checks SDL return. FavouritesBar popup uses screen virtualX/Y for multi-monitor.
This commit is contained in:
parent
4a9a5b2a97
commit
bcb985c652
11 changed files with 767 additions and 31 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
# SPDX-License-Identifier: EUPL-1.2
|
# SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
find_package(SDL3 REQUIRED)
|
find_package(SDL3 REQUIRED)
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Sql)
|
||||||
|
|
||||||
ecm_add_qml_module(gamingshellplugin URI org.kde.plasma.private.mobileshell.gamingshellplugin GENERATE_PLUGIN_SOURCE)
|
ecm_add_qml_module(gamingshellplugin URI org.kde.plasma.private.mobileshell.gamingshellplugin GENERATE_PLUGIN_SOURCE)
|
||||||
|
|
||||||
|
|
@ -15,7 +16,9 @@ target_link_libraries(gamingshellplugin PRIVATE
|
||||||
Qt::Core
|
Qt::Core
|
||||||
Qt::Qml
|
Qt::Qml
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
|
Qt::Sql
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
|
KF6::KIOGui
|
||||||
KF6::Service
|
KF6::Service
|
||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "gamelauncherprovider.h"
|
#include "gamelauncherprovider.h"
|
||||||
|
|
||||||
#include <KConfigGroup>
|
#include <KConfigGroup>
|
||||||
|
#include <KIO/ApplicationLauncherJob>
|
||||||
#include <KService>
|
#include <KService>
|
||||||
#include <KSharedConfig>
|
#include <KSharedConfig>
|
||||||
#include <KShell>
|
#include <KShell>
|
||||||
|
|
@ -12,8 +13,13 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QSqlDatabase>
|
||||||
|
#include <QSqlQuery>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
|
|
@ -90,6 +96,8 @@ void GameLauncherProvider::refresh()
|
||||||
loadDesktopGames();
|
loadDesktopGames();
|
||||||
loadSteamGames();
|
loadSteamGames();
|
||||||
loadFlatpakGames();
|
loadFlatpakGames();
|
||||||
|
loadLutrisGames();
|
||||||
|
loadHeroicGames();
|
||||||
loadRecentTimestamps();
|
loadRecentTimestamps();
|
||||||
|
|
||||||
// Deduplicate: when the same game appears from multiple sources,
|
// Deduplicate: when the same game appears from multiple sources,
|
||||||
|
|
@ -137,14 +145,14 @@ void GameLauncherProvider::launchEntry(GameEntry &entry)
|
||||||
if (entry.source == QLatin1String("desktop")) {
|
if (entry.source == QLatin1String("desktop")) {
|
||||||
auto service = KService::serviceByStorageId(entry.storageId);
|
auto service = KService::serviceByStorageId(entry.storageId);
|
||||||
if (service) {
|
if (service) {
|
||||||
QStringList args = KShell::splitArgs(service->exec());
|
auto *job = new KIO::ApplicationLauncherJob(service);
|
||||||
if (!args.isEmpty()) {
|
job->start();
|
||||||
QString program = args.takeFirst();
|
|
||||||
QProcess::startDetached(program, args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (entry.launchCommand.contains(QStringLiteral("://"))) {
|
||||||
|
// Protocol handler (e.g. heroic://launch/...) — open via xdg-open
|
||||||
|
QProcess::startDetached(QStringLiteral("xdg-open"), {entry.launchCommand});
|
||||||
} else {
|
} else {
|
||||||
QStringList parts = entry.launchCommand.split(QLatin1Char(' '));
|
QStringList parts = KShell::splitArgs(entry.launchCommand);
|
||||||
if (!parts.isEmpty()) {
|
if (!parts.isEmpty()) {
|
||||||
QString program = parts.takeFirst();
|
QString program = parts.takeFirst();
|
||||||
QProcess::startDetached(program, parts);
|
QProcess::startDetached(program, parts);
|
||||||
|
|
@ -159,19 +167,20 @@ void GameLauncherProvider::launchEntry(GameEntry &entry)
|
||||||
|
|
||||||
void GameLauncherProvider::deduplicateGames()
|
void GameLauncherProvider::deduplicateGames()
|
||||||
{
|
{
|
||||||
// Build a set of names from Steam entries (case-insensitive).
|
// Build a set of names from dedicated launcher entries (case-insensitive).
|
||||||
QSet<QString> steamNames;
|
// These have better artwork and metadata, so they win over plain .desktop entries.
|
||||||
|
QSet<QString> launcherNames;
|
||||||
for (const auto &g : std::as_const(m_allGames)) {
|
for (const auto &g : std::as_const(m_allGames)) {
|
||||||
if (g.source == QLatin1String("steam")) {
|
if (g.source == QLatin1String("steam") || g.source == QLatin1String("lutris") || g.source == QLatin1String("heroic")) {
|
||||||
steamNames.insert(g.name.toLower());
|
launcherNames.insert(g.name.toLower());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove desktop entries whose name matches a Steam entry.
|
// Remove desktop entries whose name matches a launcher entry.
|
||||||
m_allGames.erase(std::remove_if(m_allGames.begin(),
|
m_allGames.erase(std::remove_if(m_allGames.begin(),
|
||||||
m_allGames.end(),
|
m_allGames.end(),
|
||||||
[&steamNames](const GameEntry &g) {
|
[&launcherNames](const GameEntry &g) {
|
||||||
return g.source == QLatin1String("desktop") && steamNames.contains(g.name.toLower());
|
return g.source == QLatin1String("desktop") && launcherNames.contains(g.name.toLower());
|
||||||
}),
|
}),
|
||||||
m_allGames.end());
|
m_allGames.end());
|
||||||
}
|
}
|
||||||
|
|
@ -329,6 +338,171 @@ void GameLauncherProvider::loadFlatpakGames()
|
||||||
// the Game category properly).
|
// the Game category properly).
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Lutris library (SQLite) ---
|
||||||
|
|
||||||
|
void GameLauncherProvider::loadLutrisGames()
|
||||||
|
{
|
||||||
|
const QString dbPath = QDir::homePath() + QStringLiteral("/.local/share/lutris/pga.db");
|
||||||
|
if (!QFile::exists(dbPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a unique connection name to avoid conflicting with other code.
|
||||||
|
// RAII guard ensures QSqlDatabase::removeDatabase runs on every exit path.
|
||||||
|
const QString connName = QStringLiteral("lutris_games_%1").arg(reinterpret_cast<quintptr>(this));
|
||||||
|
const auto dbCleanup = qScopeGuard([&connName]() {
|
||||||
|
QSqlDatabase::removeDatabase(connName);
|
||||||
|
});
|
||||||
|
{
|
||||||
|
QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connName);
|
||||||
|
db.setDatabaseName(dbPath);
|
||||||
|
db.setConnectOptions(QStringLiteral("QSQLITE_OPEN_READONLY"));
|
||||||
|
|
||||||
|
if (!db.open()) {
|
||||||
|
qWarning() << "GameLauncherProvider: cannot open Lutris DB" << dbPath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(QStringLiteral("SELECT name, slug, runner, coverart, id FROM games WHERE installed = 1"));
|
||||||
|
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "GameLauncherProvider: Lutris query failed";
|
||||||
|
db.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString coverBase = QDir::homePath() + QStringLiteral("/.local/share/lutris/coverart/");
|
||||||
|
|
||||||
|
while (query.next()) {
|
||||||
|
GameEntry entry;
|
||||||
|
entry.name = query.value(0).toString();
|
||||||
|
const QString slug = query.value(1).toString();
|
||||||
|
const QString runner = query.value(2).toString();
|
||||||
|
const QString coverart = query.value(3).toString();
|
||||||
|
const int gameId = query.value(4).toInt();
|
||||||
|
|
||||||
|
entry.source = QStringLiteral("lutris");
|
||||||
|
entry.storageId = QStringLiteral("lutris:%1").arg(slug);
|
||||||
|
entry.icon = QStringLiteral("lutris");
|
||||||
|
entry.launchCommand = QStringLiteral("lutris lutris:rungameid/%1").arg(gameId);
|
||||||
|
entry.installed = true;
|
||||||
|
|
||||||
|
// Cover art: Lutris stores covers in ~/.local/share/lutris/coverart/
|
||||||
|
if (!coverart.isEmpty()) {
|
||||||
|
entry.artwork = coverart;
|
||||||
|
} else {
|
||||||
|
const QString coverFile = coverBase + slug + QStringLiteral(".jpg");
|
||||||
|
if (QFile::exists(coverFile)) {
|
||||||
|
entry.artwork = coverFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_allGames.append(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
// dbCleanup guard handles QSqlDatabase::removeDatabase(connName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Heroic Games Launcher (JSON) ---
|
||||||
|
|
||||||
|
void GameLauncherProvider::loadHeroicGames()
|
||||||
|
{
|
||||||
|
// Heroic stores library caches for different stores
|
||||||
|
const QString heroicBase = QDir::homePath() + QStringLiteral("/.config/heroic");
|
||||||
|
if (!QDir(heroicBase).exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check both GOG and Epic (Legendary) library caches
|
||||||
|
const QStringList libFiles = {
|
||||||
|
heroicBase + QStringLiteral("/store_cache/gog_library.json"),
|
||||||
|
heroicBase + QStringLiteral("/store_cache/legendary_library.json"),
|
||||||
|
heroicBase + QStringLiteral("/store_cache/nile_library.json"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &libPath : libFiles) {
|
||||||
|
QFile libFile(libPath);
|
||||||
|
if (!libFile.open(QIODevice::ReadOnly)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError err;
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(libFile.readAll(), &err);
|
||||||
|
if (err.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << "GameLauncherProvider: JSON parse error in" << libPath << err.errorString();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heroic library JSON: { "library": [ { "app_name": ..., "title": ..., ... } ] }
|
||||||
|
// or it can be a plain array at the top level
|
||||||
|
QJsonArray games;
|
||||||
|
if (doc.isArray()) {
|
||||||
|
games = doc.array();
|
||||||
|
} else if (doc.isObject()) {
|
||||||
|
games = doc.object().value(QStringLiteral("library")).toArray();
|
||||||
|
if (games.isEmpty()) {
|
||||||
|
games = doc.object().value(QStringLiteral("games")).toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool isGog = libPath.contains(QStringLiteral("gog"));
|
||||||
|
const bool isNile = libPath.contains(QStringLiteral("nile"));
|
||||||
|
|
||||||
|
for (const auto &val : games) {
|
||||||
|
const QJsonObject obj = val.toObject();
|
||||||
|
const QString appName = obj.value(QStringLiteral("app_name")).toString();
|
||||||
|
const QString title = obj.value(QStringLiteral("title")).toString();
|
||||||
|
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if installed
|
||||||
|
const auto isInstalled = obj.value(QStringLiteral("is_installed"));
|
||||||
|
if (isInstalled.isBool() && !isInstalled.toBool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameEntry entry;
|
||||||
|
entry.name = title;
|
||||||
|
entry.source = QStringLiteral("heroic");
|
||||||
|
entry.storageId = QStringLiteral("heroic:%1").arg(appName);
|
||||||
|
entry.icon = QStringLiteral("heroic");
|
||||||
|
entry.installed = true;
|
||||||
|
|
||||||
|
// Launch via Heroic protocol handler
|
||||||
|
if (isGog) {
|
||||||
|
entry.launchCommand = QStringLiteral("heroic://launch/gog/%1").arg(appName);
|
||||||
|
} else if (isNile) {
|
||||||
|
entry.launchCommand = QStringLiteral("heroic://launch/nile/%1").arg(appName);
|
||||||
|
} else {
|
||||||
|
entry.launchCommand = QStringLiteral("heroic://launch/legendary/%1").arg(appName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cover art: Heroic caches artwork
|
||||||
|
const QString artPath = obj.value(QStringLiteral("art_cover")).toString();
|
||||||
|
if (!artPath.isEmpty() && QFile::exists(artPath)) {
|
||||||
|
entry.artwork = artPath;
|
||||||
|
} else {
|
||||||
|
// Try Heroic's thumbnail cache
|
||||||
|
const QString thumbDir = heroicBase + QStringLiteral("/images/") + appName + QStringLiteral("/");
|
||||||
|
const QDir thumbs(thumbDir);
|
||||||
|
if (thumbs.exists()) {
|
||||||
|
const auto images = thumbs.entryList({QStringLiteral("*.jpg"), QStringLiteral("*.png"), QStringLiteral("*.webp")}, QDir::Files);
|
||||||
|
if (!images.isEmpty()) {
|
||||||
|
entry.artwork = thumbDir + images.first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_allGames.append(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString GameLauncherProvider::filterString() const
|
QString GameLauncherProvider::filterString() const
|
||||||
{
|
{
|
||||||
return m_filterString;
|
return m_filterString;
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ private:
|
||||||
void loadDesktopGames();
|
void loadDesktopGames();
|
||||||
void loadSteamGames();
|
void loadSteamGames();
|
||||||
void loadFlatpakGames();
|
void loadFlatpakGames();
|
||||||
|
void loadLutrisGames();
|
||||||
|
void loadHeroicGames();
|
||||||
void deduplicateGames();
|
void deduplicateGames();
|
||||||
void loadRecentTimestamps();
|
void loadRecentTimestamps();
|
||||||
void saveRecentTimestamp(const QString &storageId, const QDateTime &when);
|
void saveRecentTimestamp(const QString &storageId, const QDateTime &when);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
#include <SDL3/SDL_power.h>
|
#include <SDL3/SDL_power.h>
|
||||||
#include <SDL3/SDL_properties.h>
|
#include <SDL3/SDL_properties.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
GamepadDevice::GamepadDevice(SDL_Gamepad *pad, int id, QObject *parent)
|
GamepadDevice::GamepadDevice(SDL_Gamepad *pad, int id, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_pad(pad)
|
, m_pad(pad)
|
||||||
|
|
@ -96,16 +98,20 @@ void GamepadDevice::setPlayerIndex(int index)
|
||||||
if (!m_pad) {
|
if (!m_pad) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SDL_SetGamepadPlayerIndex(m_pad, index);
|
if (SDL_SetGamepadPlayerIndex(m_pad, index)) {
|
||||||
Q_EMIT playerIndexChanged();
|
Q_EMIT playerIndexChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GamepadDevice::rumble(int lowFreqMs, int highFreqMs, int durationMs)
|
bool GamepadDevice::rumble(int lowIntensity, int highIntensity, int durationMs)
|
||||||
{
|
{
|
||||||
if (!m_pad) {
|
if (!m_pad) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return SDL_RumbleGamepad(m_pad, static_cast<uint16_t>(lowFreqMs), static_cast<uint16_t>(highFreqMs), static_cast<uint32_t>(durationMs));
|
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::setLED(int r, int g, int b)
|
bool GamepadDevice::setLED(int r, int g, int b)
|
||||||
|
|
@ -113,7 +119,10 @@ bool GamepadDevice::setLED(int r, int g, int b)
|
||||||
if (!m_pad) {
|
if (!m_pad) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return SDL_SetGamepadLED(m_pad, static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Gamepad *GamepadDevice::sdlGamepad() const
|
SDL_Gamepad *GamepadDevice::sdlGamepad() const
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public:
|
||||||
int playerIndex() const;
|
int playerIndex() const;
|
||||||
void setPlayerIndex(int index);
|
void setPlayerIndex(int index);
|
||||||
|
|
||||||
Q_INVOKABLE bool rumble(int lowFreqMs, int highFreqMs, int durationMs);
|
Q_INVOKABLE bool rumble(int lowIntensity, int highIntensity, int durationMs);
|
||||||
Q_INVOKABLE bool setLED(int r, int g, int b);
|
Q_INVOKABLE bool setLED(int r, int g, int b);
|
||||||
|
|
||||||
SDL_Gamepad *sdlGamepad() const;
|
SDL_Gamepad *sdlGamepad() const;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@
|
||||||
#include <SDL3/SDL_gamepad.h>
|
#include <SDL3/SDL_gamepad.h>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
GamepadManager::GamepadManager(QObject *parent)
|
GamepadManager::GamepadManager(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
|
|
@ -21,6 +24,16 @@ GamepadManager::~GamepadManager()
|
||||||
stop();
|
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 ---
|
// --- QAbstractListModel ---
|
||||||
|
|
||||||
int GamepadManager::rowCount(const QModelIndex &parent) const
|
int GamepadManager::rowCount(const QModelIndex &parent) const
|
||||||
|
|
@ -180,7 +193,7 @@ void GamepadManager::poll()
|
||||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
|
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
|
||||||
int idx = indexForInstanceId(event.gaxis.which);
|
int idx = indexForInstanceId(event.gaxis.which);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
float normalized = static_cast<float>(event.gaxis.value) / 32767.0f;
|
float normalized = std::clamp(static_cast<float>(event.gaxis.value) / 32767.0f, -1.0f, 1.0f);
|
||||||
Q_EMIT axisChanged(event.gaxis.axis, normalized, idx);
|
Q_EMIT axisChanged(event.gaxis.axis, normalized, idx);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -192,9 +205,8 @@ void GamepadManager::poll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh battery state periodically (every ~5 seconds = 300 frames)
|
// Refresh battery state periodically (every ~5 seconds = 300 frames)
|
||||||
static int batteryCounter = 0;
|
if (++m_batteryCounter >= 300) {
|
||||||
if (++batteryCounter >= 300) {
|
m_batteryCounter = 0;
|
||||||
batteryCounter = 0;
|
|
||||||
for (auto *dev : std::as_const(m_gamepads)) {
|
for (auto *dev : std::as_const(m_gamepads)) {
|
||||||
dev->refreshBattery();
|
dev->refreshBattery();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <qqmlregistration.h>
|
#include <qqmlregistration.h>
|
||||||
|
|
||||||
|
class QQmlEngine;
|
||||||
|
class QJSEngine;
|
||||||
class GamepadDevice;
|
class GamepadDevice;
|
||||||
|
|
||||||
class GamepadManager : public QAbstractListModel
|
class GamepadManager : public QAbstractListModel
|
||||||
|
|
@ -24,6 +26,8 @@ public:
|
||||||
explicit GamepadManager(QObject *parent = nullptr);
|
explicit GamepadManager(QObject *parent = nullptr);
|
||||||
~GamepadManager() override;
|
~GamepadManager() override;
|
||||||
|
|
||||||
|
static GamepadManager *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
|
||||||
|
|
||||||
enum Roles {
|
enum Roles {
|
||||||
DeviceRole = Qt::UserRole + 1,
|
DeviceRole = Qt::UserRole + 1,
|
||||||
NameRole,
|
NameRole,
|
||||||
|
|
@ -95,6 +99,7 @@ private:
|
||||||
|
|
||||||
bool m_active = false;
|
bool m_active = false;
|
||||||
bool m_sdlInitialized = false;
|
bool m_sdlInitialized = false;
|
||||||
|
int m_batteryCounter = 0;
|
||||||
QTimer m_pollTimer;
|
QTimer m_pollTimer;
|
||||||
QList<GamepadDevice *> m_gamepads;
|
QList<GamepadDevice *> m_gamepads;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
||||||
ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
||||||
qml/gaming/GameCenterOverlay.qml
|
qml/gaming/GameCenterOverlay.qml
|
||||||
qml/gaming/GamingHUD.qml
|
qml/gaming/GamingHUD.qml
|
||||||
|
qml/gaming/GamingQuickSettings.qml
|
||||||
qml/gaming/RunningGamesView.qml
|
qml/gaming/RunningGamesView.qml
|
||||||
PATH gaming
|
PATH gaming
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -707,15 +707,19 @@ MouseArea {
|
||||||
if (!targetDelegate) return 0
|
if (!targetDelegate) return 0
|
||||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||||
var win = targetDelegate.Window.window
|
var win = targetDelegate.Window.window
|
||||||
var scrW = win && win.screen ? win.screen.width : Screen.width
|
var screenLeft = win && win.screen ? win.screen.virtualX : 0
|
||||||
return Math.max(0, Math.min(scrW - width, delegateGlobal.x + (targetDelegate.width - width) / 2))
|
var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
|
||||||
|
var centered = delegateGlobal.x + (targetDelegate.width - width) / 2
|
||||||
|
return Math.max(screenLeft, Math.min(screenRight - width, centered))
|
||||||
}
|
}
|
||||||
y: {
|
y: {
|
||||||
if (!targetDelegate) return 0
|
if (!targetDelegate) return 0
|
||||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||||
var win = targetDelegate.Window.window
|
var win = targetDelegate.Window.window
|
||||||
var scrH = win && win.screen ? win.screen.height : Screen.height
|
var screenTop = win && win.screen ? win.screen.virtualY : 0
|
||||||
return Math.max(0, Math.min(scrH - height, delegateGlobal.y - height - Kirigami.Units.smallSpacing))
|
var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
|
||||||
|
var above = delegateGlobal.y - height - Kirigami.Units.smallSpacing
|
||||||
|
return Math.max(screenTop, Math.min(screenBottom - height, above))
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowingChanged: {
|
onShowingChanged: {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ import org.kde.plasma.private.mobileshell as MobileShell
|
||||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
||||||
|
import org.kde.plasma.private.mobileshell.screenbrightnessplugin as ScreenBrightness
|
||||||
import org.kde.layershell 1.0 as LayerShell
|
import org.kde.layershell 1.0 as LayerShell
|
||||||
|
import org.kde.plasma.clock
|
||||||
|
|
||||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||||
|
|
||||||
|
|
@ -76,6 +78,32 @@ Window {
|
||||||
enabled: root.visible
|
enabled: root.visible
|
||||||
|
|
||||||
function onButtonPressed(button, gamepadIndex) {
|
function onButtonPressed(button, gamepadIndex) {
|
||||||
|
// Route to quick settings panel when open
|
||||||
|
if (quickSettings.opened) {
|
||||||
|
switch (button) {
|
||||||
|
case GamingShell.GamepadManager.ButtonDPadUp:
|
||||||
|
quickSettings.gamepadUp()
|
||||||
|
return
|
||||||
|
case GamingShell.GamepadManager.ButtonDPadDown:
|
||||||
|
quickSettings.gamepadDown()
|
||||||
|
return
|
||||||
|
case GamingShell.GamepadManager.ButtonDPadLeft:
|
||||||
|
quickSettings.gamepadLeft()
|
||||||
|
return
|
||||||
|
case GamingShell.GamepadManager.ButtonDPadRight:
|
||||||
|
quickSettings.gamepadRight()
|
||||||
|
return
|
||||||
|
case GamingShell.GamepadManager.ButtonA:
|
||||||
|
quickSettings.gamepadAccept()
|
||||||
|
return
|
||||||
|
case GamingShell.GamepadManager.ButtonB:
|
||||||
|
case GamingShell.GamepadManager.ButtonBack:
|
||||||
|
quickSettings.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return // eat all other buttons while panel is open
|
||||||
|
}
|
||||||
|
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case GamingShell.GamepadManager.ButtonDPadUp:
|
case GamingShell.GamepadManager.ButtonDPadUp:
|
||||||
if (grid.activeFocus) {
|
if (grid.activeFocus) {
|
||||||
|
|
@ -130,6 +158,9 @@ Window {
|
||||||
searchField.forceActiveFocus()
|
searchField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case GamingShell.GamepadManager.ButtonBack:
|
||||||
|
quickSettings.toggle()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,6 +185,21 @@ Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateByStick() {
|
function navigateByStick() {
|
||||||
|
// Route stick to quick settings when open
|
||||||
|
if (quickSettings.opened) {
|
||||||
|
if (stickState.leftY < -stickState.deadzone) {
|
||||||
|
quickSettings.gamepadUp()
|
||||||
|
} else if (stickState.leftY > stickState.deadzone) {
|
||||||
|
quickSettings.gamepadDown()
|
||||||
|
}
|
||||||
|
if (stickState.leftX < -stickState.deadzone) {
|
||||||
|
quickSettings.gamepadLeft()
|
||||||
|
} else if (stickState.leftX > stickState.deadzone) {
|
||||||
|
quickSettings.gamepadRight()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (stickState.leftY < -stickState.deadzone) {
|
if (stickState.leftY < -stickState.deadzone) {
|
||||||
if (grid.activeFocus) {
|
if (grid.activeFocus) {
|
||||||
if (grid.currentIndex < grid.columns && runningGames.hasTasks) {
|
if (grid.currentIndex < grid.columns && runningGames.hasTasks) {
|
||||||
|
|
@ -202,7 +248,7 @@ Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cycle through source filter tabs (All → Steam → Desktop → All …)
|
// Cycle through source filter tabs (All → Steam → Desktop → All …)
|
||||||
readonly property var _sourceFilters: ["", "steam", "desktop"]
|
readonly property var _sourceFilters: ["", "steam", "desktop", "lutris", "heroic"]
|
||||||
function cycleSourceFilter(direction) {
|
function cycleSourceFilter(direction) {
|
||||||
var current = _sourceFilters.indexOf(
|
var current = _sourceFilters.indexOf(
|
||||||
GamingShell.GameLauncherProvider.sourceFilter)
|
GamingShell.GameLauncherProvider.sourceFilter)
|
||||||
|
|
@ -238,13 +284,63 @@ Window {
|
||||||
// ---- header ----
|
// ---- header ----
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
Kirigami.Heading {
|
Kirigami.Heading {
|
||||||
Layout.fillWidth: true
|
|
||||||
text: i18n("Game Center")
|
text: i18n("Game Center")
|
||||||
level: 1
|
level: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// ---- system status indicators ----
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
Clock { id: wallClock }
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: Qt.formatTime(wallClock.dateTime,
|
||||||
|
MobileShell.ShellUtil.isSystem24HourFormat ? "h:mm" : "h:mm ap")
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
source: MobileShell.AudioInfo.icon
|
||||||
|
visible: MobileShell.AudioInfo.isVisible
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileShell.InternetIndicator {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileShell.BluetoothIndicator {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileShell.BatteryIndicator {
|
||||||
|
textPixelSize: Kirigami.Units.gridUnit * 0.55
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- quick settings button ----
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "configure"
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.text: i18n("Quick Settings")
|
||||||
|
onClicked: quickSettings.open()
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
icon.name: "window-close"
|
icon.name: "window-close"
|
||||||
text: i18n("Exit Gaming Mode")
|
text: i18n("Exit Gaming Mode")
|
||||||
|
|
@ -372,6 +468,16 @@ Window {
|
||||||
width: implicitWidth
|
width: implicitWidth
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop"
|
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop"
|
||||||
}
|
}
|
||||||
|
QQC2.TabButton {
|
||||||
|
text: "Lutris"
|
||||||
|
width: implicitWidth
|
||||||
|
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "lutris"
|
||||||
|
}
|
||||||
|
QQC2.TabButton {
|
||||||
|
text: "Heroic"
|
||||||
|
width: implicitWidth
|
||||||
|
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "heroic"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -534,6 +640,8 @@ Window {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
text: source === "steam" ? "Steam"
|
text: source === "steam" ? "Steam"
|
||||||
: source === "flatpak" ? "Flatpak"
|
: source === "flatpak" ? "Flatpak"
|
||||||
|
: source === "lutris" ? "Lutris"
|
||||||
|
: source === "heroic" ? "Heroic"
|
||||||
: ""
|
: ""
|
||||||
visible: source !== "desktop"
|
visible: source !== "desktop"
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||||
|
|
@ -588,8 +696,8 @@ Window {
|
||||||
// Gamepad legend
|
// Gamepad legend
|
||||||
PC3.Label {
|
PC3.Label {
|
||||||
text: runningGames.hasTasks
|
text: runningGames.hasTasks
|
||||||
? i18n("A: Select X: Close B: Back Y: Exit LB/RB: Filter ☰: Search")
|
? i18n("A: Select X: Close B: Back Y: Exit ⊞: Settings ☰: Search")
|
||||||
: i18n("A: Select B: Back Y: Exit LB/RB: Filter ☰: Search")
|
: i18n("A: Select B: Back Y: Exit LB/RB: Filter ⊞: Settings ☰: Search")
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||||
opacity: 0.5
|
opacity: 0.5
|
||||||
}
|
}
|
||||||
|
|
@ -597,6 +705,12 @@ Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quick settings slide-out panel
|
||||||
|
GamingQuickSettings {
|
||||||
|
id: quickSettings
|
||||||
|
z: 50
|
||||||
|
}
|
||||||
|
|
||||||
// Launch transition: brief fade to black, then dismiss
|
// Launch transition: brief fade to black, then dismiss
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: launchCurtain
|
id: launchCurtain
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,412 @@
|
||||||
|
// 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.screenbrightnessplugin as ScreenBrightness
|
||||||
|
import org.kde.plasma.private.volume
|
||||||
|
import org.kde.plasma.networkmanagement as PlasmaNM
|
||||||
|
import org.kde.bluezqt 1.0 as BluezQt
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
property bool opened: false
|
||||||
|
|
||||||
|
// Focusable controls for gamepad navigation
|
||||||
|
property var _controls: []
|
||||||
|
property int _focusIndex: 0
|
||||||
|
|
||||||
|
function _buildControlsList() {
|
||||||
|
var list = []
|
||||||
|
if (screenBrightness.brightnessAvailable) list.push(brightnessSlider)
|
||||||
|
if (PreferredDevice.sink) list.push(volumeSlider)
|
||||||
|
list.push(wifiSwitch)
|
||||||
|
list.push(btSwitch)
|
||||||
|
list.push(airplaneSwitch)
|
||||||
|
_controls = list
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
opened = true
|
||||||
|
_buildControlsList()
|
||||||
|
_focusIndex = 0
|
||||||
|
_highlightCurrent()
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
opened = false
|
||||||
|
}
|
||||||
|
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 (ctrl instanceof PC3.Slider) {
|
||||||
|
ctrl.decrease()
|
||||||
|
ctrl.moved()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gamepadRight() {
|
||||||
|
var ctrl = _controls[_focusIndex]
|
||||||
|
if (ctrl instanceof PC3.Slider) {
|
||||||
|
ctrl.increase()
|
||||||
|
ctrl.moved()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gamepadAccept() {
|
||||||
|
var ctrl = _controls[_focusIndex]
|
||||||
|
if (ctrl instanceof QQC2.Switch) {
|
||||||
|
ctrl.toggle()
|
||||||
|
ctrl.toggled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
|
||||||
|
// ---- 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus highlight
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus highlight
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
source: "audio-volume-high"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 A: Toggle B: Close")
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
|
||||||
|
opacity: 0.5
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillHeight: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue