mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-07-29 07:44:45 +00:00
Add MangoHud overlay, FPS cap, and game pinning
Inject MangoHud when launching desktop and command-line games. MANGOHUD_CONFIG is built per-launch via a QProcess instance so env vars are isolated to each child process — qputenv is not used. Global overlay toggle and FPS cap (Off/30/40/60) are stored as properties on GameLauncherProvider and reflected in the quick settings panel. Per-game overrides stored in plasmamobilerc under [GamingPerGame/<storageId>] take precedence over the globals at launch time. Games can be pinned to the top of the grid. The pinned set is persisted in plasmamobilerc [GamingPinned] and restored on start. applyFilter() uses stable_sort so pinned games float to the top while alphabetical order is preserved within each group.
This commit is contained in:
parent
26945e83fa
commit
b42ef44e23
3 changed files with 337 additions and 10 deletions
|
|
@ -18,7 +18,9 @@
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QLocale>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
#include <QProcessEnvironment>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSqlDatabase>
|
#include <QSqlDatabase>
|
||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
|
|
@ -28,6 +30,8 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed");
|
static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed");
|
||||||
|
static const QString s_pinnedGroup = QStringLiteral("GamingPinned");
|
||||||
|
static const QString s_perGameGroup = QStringLiteral("GamingPerGame");
|
||||||
static const QString s_waydroidGamingGroup = QStringLiteral("WaydroidGaming");
|
static const QString s_waydroidGamingGroup = QStringLiteral("WaydroidGaming");
|
||||||
static const QString s_gameShellPackagesKey = QStringLiteral("gameShellPackages");
|
static const QString s_gameShellPackagesKey = QStringLiteral("gameShellPackages");
|
||||||
|
|
||||||
|
|
@ -266,6 +270,9 @@ GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
||||||
m_pendingLaunchTimer.setInterval(15000);
|
m_pendingLaunchTimer.setInterval(15000);
|
||||||
m_pendingLaunchTimer.setSingleShot(true);
|
m_pendingLaunchTimer.setSingleShot(true);
|
||||||
connect(&m_pendingLaunchTimer, &QTimer::timeout, this, &GameLauncherProvider::clearPendingLaunch);
|
connect(&m_pendingLaunchTimer, &QTimer::timeout, this, &GameLauncherProvider::clearPendingLaunch);
|
||||||
|
m_mangohudPath = QStandardPaths::findExecutable(QStringLiteral("mangohud"));
|
||||||
|
m_mangohudAvailable = !m_mangohudPath.isEmpty();
|
||||||
|
loadPinnedGames();
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,10 +298,16 @@ QVariant GameLauncherProvider::data(const QModelIndex &index, int role) const
|
||||||
return g.storageId;
|
return g.storageId;
|
||||||
case LaunchCommandRole:
|
case LaunchCommandRole:
|
||||||
return g.launchCommand;
|
return g.launchCommand;
|
||||||
|
case LaunchMethodRole:
|
||||||
|
return launchMethodForEntry(g);
|
||||||
case ArtworkRole:
|
case ArtworkRole:
|
||||||
return g.artwork;
|
return g.artwork;
|
||||||
|
case LastPlayedTextRole:
|
||||||
|
return formatLastPlayed(g.lastPlayed);
|
||||||
case InstalledRole:
|
case InstalledRole:
|
||||||
return g.installed;
|
return g.installed;
|
||||||
|
case PinnedRole:
|
||||||
|
return m_pinnedGames.contains(g.storageId);
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -307,8 +320,11 @@ QHash<int, QByteArray> GameLauncherProvider::roleNames() const
|
||||||
{SourceRole, "source"},
|
{SourceRole, "source"},
|
||||||
{StorageIdRole, "storageId"},
|
{StorageIdRole, "storageId"},
|
||||||
{LaunchCommandRole, "launchCommand"},
|
{LaunchCommandRole, "launchCommand"},
|
||||||
|
{LaunchMethodRole, "launchMethod"},
|
||||||
{ArtworkRole, "artwork"},
|
{ArtworkRole, "artwork"},
|
||||||
|
{LastPlayedTextRole, "lastPlayedText"},
|
||||||
{InstalledRole, "installed"},
|
{InstalledRole, "installed"},
|
||||||
|
{PinnedRole, "pinned"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,10 +407,68 @@ void GameLauncherProvider::launchByStorageId(const QString &storageId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantMap GameLauncherProvider::gameDetails(const QString &storageId) const
|
||||||
|
{
|
||||||
|
const int entryIndex = findEntryIndexByStorageId(storageId);
|
||||||
|
if (entryIndex < 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &entry = m_allGames.at(entryIndex);
|
||||||
|
return {
|
||||||
|
{QStringLiteral("name"), entry.name},
|
||||||
|
{QStringLiteral("icon"), entry.icon},
|
||||||
|
{QStringLiteral("source"), entry.source},
|
||||||
|
{QStringLiteral("storageId"), entry.storageId},
|
||||||
|
{QStringLiteral("launchMethod"), launchMethodForEntry(entry)},
|
||||||
|
{QStringLiteral("artwork"), entry.artwork},
|
||||||
|
{QStringLiteral("lastPlayedText"), formatLastPlayed(entry.lastPlayed)},
|
||||||
|
{QStringLiteral("installed"), entry.installed},
|
||||||
|
{QStringLiteral("pinned"), m_pinnedGames.contains(entry.storageId)},
|
||||||
|
{QStringLiteral("perGameFpsLimit"), perGameFpsLimit(entry.storageId)},
|
||||||
|
{QStringLiteral("perGameOverlayState"), perGameOverlayState(entry.storageId)},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameLauncherProvider::openSourceApp(const QString &source)
|
||||||
|
{
|
||||||
|
QString program;
|
||||||
|
QString displayName;
|
||||||
|
|
||||||
|
if (source == QLatin1String("steam")) {
|
||||||
|
program = QStringLiteral("steam");
|
||||||
|
displayName = QStringLiteral("Steam");
|
||||||
|
} else if (source == QLatin1String("lutris")) {
|
||||||
|
program = QStringLiteral("lutris");
|
||||||
|
displayName = QStringLiteral("Lutris");
|
||||||
|
} else if (source == QLatin1String("heroic")) {
|
||||||
|
program = QStringLiteral("heroic");
|
||||||
|
displayName = QStringLiteral("Heroic");
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLastLaunchError();
|
||||||
|
|
||||||
|
if (!QProcess::startDetached(program, {})) {
|
||||||
|
markLaunchFailed(displayName, QStringLiteral("Unable to start %1").arg(program));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void GameLauncherProvider::launchEntry(GameEntry &entry)
|
void GameLauncherProvider::launchEntry(GameEntry &entry)
|
||||||
{
|
{
|
||||||
clearLastLaunchError();
|
clearLastLaunchError();
|
||||||
|
|
||||||
|
const KConfigGroup pgParent(m_config.data(), s_perGameGroup);
|
||||||
|
const KConfigGroup pgGroup(&pgParent, entry.storageId);
|
||||||
|
const int pgFpsLimit = pgGroup.readEntry("fpsLimit", -1);
|
||||||
|
const int pgOverlayState = pgGroup.readEntry("overlayState", -1);
|
||||||
|
const int effectiveFps = (pgFpsLimit >= 0) ? pgFpsLimit : m_fpsLimit;
|
||||||
|
const bool effectiveOverlay = (pgOverlayState >= 0) ? (pgOverlayState == 1) : m_overlayEnabled;
|
||||||
|
|
||||||
if (entry.source == QLatin1String("desktop") || entry.source == QLatin1String("waydroid")) {
|
if (entry.source == QLatin1String("desktop") || entry.source == QLatin1String("waydroid")) {
|
||||||
auto service = KService::serviceByStorageId(entry.storageId);
|
auto service = KService::serviceByStorageId(entry.storageId);
|
||||||
if (!service) {
|
if (!service) {
|
||||||
|
|
@ -402,6 +476,27 @@ void GameLauncherProvider::launchEntry(GameEntry &entry)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap native desktop entries with mangohud when overlay or FPS cap is active.
|
||||||
|
// Waydroid (Android) games don't benefit from it, so skip them.
|
||||||
|
if (entry.source == QLatin1String("desktop") && m_mangohudAvailable && (effectiveOverlay || effectiveFps > 0)) {
|
||||||
|
QString exec = service->exec();
|
||||||
|
// Strip KDE .desktop field-code placeholders (%u, %U, %f, %F, %i, %c, %k …)
|
||||||
|
exec.remove(QRegularExpression(QStringLiteral("%[a-zA-Z]")));
|
||||||
|
KShell::Errors parseError = KShell::NoError;
|
||||||
|
QStringList parts = KShell::splitArgs(exec.simplified(), KShell::NoOptions, &parseError);
|
||||||
|
if (parseError == KShell::NoError && !parts.isEmpty()) {
|
||||||
|
const QString program = parts.takeFirst();
|
||||||
|
qint64 pid = 0;
|
||||||
|
if (launchWithMangohud(program, parts, effectiveOverlay, effectiveFps, &pid)) {
|
||||||
|
markLaunchSucceeded(entry.storageId, entry.name);
|
||||||
|
} else {
|
||||||
|
markLaunchFailed(entry.name, QStringLiteral("Failed to launch %1 with mangohud").arg(program));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Exec parsing failed — fall through to the standard KIO job without overlay.
|
||||||
|
}
|
||||||
|
|
||||||
auto *job = new KIO::ApplicationLauncherJob(service);
|
auto *job = new KIO::ApplicationLauncherJob(service);
|
||||||
connect(job, &KJob::result, this, [this, job, storageId = entry.storageId, name = entry.name]() {
|
connect(job, &KJob::result, this, [this, job, storageId = entry.storageId, name = entry.name]() {
|
||||||
if (job->error() != 0) {
|
if (job->error() != 0) {
|
||||||
|
|
@ -426,7 +521,13 @@ void GameLauncherProvider::launchEntry(GameEntry &entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
QString program = parts.takeFirst();
|
QString program = parts.takeFirst();
|
||||||
if (!QProcess::startDetached(program, parts)) {
|
if (m_mangohudAvailable && (effectiveOverlay || effectiveFps > 0)) {
|
||||||
|
qint64 pid = 0;
|
||||||
|
if (!launchWithMangohud(program, parts, effectiveOverlay, effectiveFps, &pid)) {
|
||||||
|
markLaunchFailed(entry.name, QStringLiteral("Unable to start %1 with mangohud").arg(program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!QProcess::startDetached(program, parts)) {
|
||||||
markLaunchFailed(entry.name, QStringLiteral("Unable to start %1").arg(program));
|
markLaunchFailed(entry.name, QStringLiteral("Unable to start %1").arg(program));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -818,6 +919,46 @@ bool GameLauncherProvider::overlayEnabled() const
|
||||||
return m_overlayEnabled;
|
return m_overlayEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameLauncherProvider::mangohudAvailable() const
|
||||||
|
{
|
||||||
|
return m_mangohudAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameLauncherProvider::fpsLimit() const
|
||||||
|
{
|
||||||
|
return m_fpsLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameLauncherProvider::setFpsLimit(int limit)
|
||||||
|
{
|
||||||
|
if (m_fpsLimit == limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_fpsLimit = limit;
|
||||||
|
Q_EMIT fpsLimitChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameLauncherProvider::launchWithMangohud(const QString &program, const QStringList &args, bool overlayEnabled, int fpsLimit, qint64 *pid)
|
||||||
|
{
|
||||||
|
QStringList config;
|
||||||
|
if (!overlayEnabled) {
|
||||||
|
config << QStringLiteral("no_display");
|
||||||
|
}
|
||||||
|
if (fpsLimit > 0) {
|
||||||
|
config << QStringLiteral("fps_limit=%1").arg(fpsLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
QProcess proc;
|
||||||
|
if (!config.isEmpty()) {
|
||||||
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||||
|
env.insert(QStringLiteral("MANGOHUD_CONFIG"), config.join(QLatin1Char(',')));
|
||||||
|
proc.setProcessEnvironment(env);
|
||||||
|
}
|
||||||
|
proc.setProgram(m_mangohudPath);
|
||||||
|
proc.setArguments(QStringList{program} + args);
|
||||||
|
return proc.startDetached(pid);
|
||||||
|
}
|
||||||
|
|
||||||
void GameLauncherProvider::setOverlayEnabled(bool enabled)
|
void GameLauncherProvider::setOverlayEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
if (m_overlayEnabled == enabled) {
|
if (m_overlayEnabled == enabled) {
|
||||||
|
|
@ -825,15 +966,6 @@ void GameLauncherProvider::setOverlayEnabled(bool enabled)
|
||||||
}
|
}
|
||||||
m_overlayEnabled = enabled;
|
m_overlayEnabled = enabled;
|
||||||
Q_EMIT overlayEnabledChanged();
|
Q_EMIT overlayEnabledChanged();
|
||||||
|
|
||||||
// Set/unset MangoHud environment variables for child processes
|
|
||||||
if (enabled) {
|
|
||||||
qputenv("MANGOHUD", "1");
|
|
||||||
qputenv("MANGOHUD_DLSYM", "1");
|
|
||||||
} else {
|
|
||||||
qunsetenv("MANGOHUD");
|
|
||||||
qunsetenv("MANGOHUD_DLSYM");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameLauncherProvider::applyFilter()
|
void GameLauncherProvider::applyFilter()
|
||||||
|
|
@ -851,6 +983,10 @@ void GameLauncherProvider::applyFilter()
|
||||||
m_games.append(g);
|
m_games.append(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::stable_sort(m_games.begin(), m_games.end(), [this](const GameEntry &a, const GameEntry &b) {
|
||||||
|
return m_pinnedGames.contains(a.storageId) > m_pinnedGames.contains(b.storageId);
|
||||||
|
});
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
Q_EMIT countChanged();
|
Q_EMIT countChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -873,6 +1009,129 @@ void GameLauncherProvider::saveRecentTimestamp(const QString &storageId, const Q
|
||||||
group.sync();
|
group.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameLauncherProvider::loadPinnedGames()
|
||||||
|
{
|
||||||
|
const KConfigGroup group(m_config, s_pinnedGroup);
|
||||||
|
const QStringList list = group.readEntry(QStringLiteral("pinned"), QStringList{});
|
||||||
|
m_pinnedGames = QSet<QString>(list.begin(), list.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameLauncherProvider::togglePin(const QString &storageId)
|
||||||
|
{
|
||||||
|
if (storageId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_pinnedGames.contains(storageId)) {
|
||||||
|
m_pinnedGames.remove(storageId);
|
||||||
|
} else {
|
||||||
|
m_pinnedGames.insert(storageId);
|
||||||
|
}
|
||||||
|
KConfigGroup group(m_config, s_pinnedGroup);
|
||||||
|
group.writeEntry(QStringLiteral("pinned"), QStringList(m_pinnedGames.begin(), m_pinnedGames.end()));
|
||||||
|
group.sync();
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameLauncherProvider::perGameFpsLimit(const QString &storageId) const
|
||||||
|
{
|
||||||
|
const KConfigGroup parent(m_config.data(), s_perGameGroup);
|
||||||
|
const KConfigGroup group(&parent, storageId);
|
||||||
|
return group.readEntry("fpsLimit", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameLauncherProvider::setPerGameFpsLimit(const QString &storageId, int limit)
|
||||||
|
{
|
||||||
|
if (storageId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
KConfigGroup parent(m_config.data(), s_perGameGroup);
|
||||||
|
KConfigGroup group(&parent, storageId);
|
||||||
|
if (limit < 0) {
|
||||||
|
group.deleteEntry("fpsLimit");
|
||||||
|
} else {
|
||||||
|
group.writeEntry("fpsLimit", limit);
|
||||||
|
}
|
||||||
|
group.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameLauncherProvider::perGameOverlayState(const QString &storageId) const
|
||||||
|
{
|
||||||
|
const KConfigGroup parent(m_config.data(), s_perGameGroup);
|
||||||
|
const KConfigGroup group(&parent, storageId);
|
||||||
|
return group.readEntry("overlayState", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameLauncherProvider::setPerGameOverlayState(const QString &storageId, int state)
|
||||||
|
{
|
||||||
|
if (storageId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
KConfigGroup parent(m_config.data(), s_perGameGroup);
|
||||||
|
KConfigGroup group(&parent, storageId);
|
||||||
|
if (state < 0) {
|
||||||
|
group.deleteEntry("overlayState");
|
||||||
|
} else {
|
||||||
|
group.writeEntry("overlayState", state);
|
||||||
|
}
|
||||||
|
group.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameLauncherProvider::clearLastPlayed(const QString &storageId)
|
||||||
|
{
|
||||||
|
if (storageId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KConfigGroup group(m_config, s_recentGroup);
|
||||||
|
if (!group.hasKey(storageId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
group.deleteEntry(storageId);
|
||||||
|
group.sync();
|
||||||
|
|
||||||
|
const int entryIndex = findEntryIndexByStorageId(storageId);
|
||||||
|
if (entryIndex >= 0) {
|
||||||
|
m_allGames[entryIndex].lastPlayed = QDateTime();
|
||||||
|
const int filteredIndex = [&] {
|
||||||
|
for (int i = 0; i < m_games.size(); ++i) {
|
||||||
|
if (m_games.at(i).storageId == storageId) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}();
|
||||||
|
if (filteredIndex >= 0) {
|
||||||
|
m_games[filteredIndex].lastPlayed = QDateTime();
|
||||||
|
const QModelIndex idx = index(filteredIndex);
|
||||||
|
Q_EMIT dataChanged(idx, idx, {LastPlayedTextRole});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT recentGamesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GameLauncherProvider::launchMethodForEntry(const GameEntry &entry) const
|
||||||
|
{
|
||||||
|
if (entry.source == QLatin1String("desktop") || entry.source == QLatin1String("waydroid")) {
|
||||||
|
return QStringLiteral("desktop-entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.launchCommand.contains(QStringLiteral("://"))) {
|
||||||
|
return QStringLiteral("protocol");
|
||||||
|
}
|
||||||
|
|
||||||
|
return QStringLiteral("command");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GameLauncherProvider::formatLastPlayed(const QDateTime &when) const
|
||||||
|
{
|
||||||
|
if (!when.isValid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return QLocale().toString(when, QLocale::ShortFormat);
|
||||||
|
}
|
||||||
|
|
||||||
void GameLauncherProvider::clearPendingLaunch()
|
void GameLauncherProvider::clearPendingLaunch()
|
||||||
{
|
{
|
||||||
if (!m_launchPending && m_pendingLaunchName.isEmpty()) {
|
if (!m_launchPending && m_pendingLaunchName.isEmpty()) {
|
||||||
|
|
@ -917,6 +1176,7 @@ void GameLauncherProvider::markLaunchSucceeded(const QString &storageId, const Q
|
||||||
|
|
||||||
setPendingLaunch(name);
|
setPendingLaunch(name);
|
||||||
Q_EMIT gameLaunched(name);
|
Q_EMIT gameLaunched(name);
|
||||||
|
Q_EMIT recentGamesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameLauncherProvider::markLaunchFailed(const QString &name, const QString &error)
|
void GameLauncherProvider::markLaunchFailed(const QString &name, const QString &error)
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QSet>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QVariantMap>
|
||||||
#include <qqmlregistration.h>
|
#include <qqmlregistration.h>
|
||||||
|
|
||||||
#include <KConfigWatcher>
|
#include <KConfigWatcher>
|
||||||
|
|
@ -24,6 +26,8 @@ class GameLauncherProvider : public QAbstractListModel
|
||||||
Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged)
|
Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged)
|
||||||
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
|
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
|
||||||
Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged)
|
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(bool launchPending READ launchPending NOTIFY launchPendingChanged)
|
||||||
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
|
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
|
||||||
Q_PROPERTY(QString lastLaunchError READ lastLaunchError NOTIFY lastLaunchErrorChanged)
|
Q_PROPERTY(QString lastLaunchError READ lastLaunchError NOTIFY lastLaunchErrorChanged)
|
||||||
|
|
@ -37,8 +41,11 @@ public:
|
||||||
SourceRole, // "desktop", "waydroid", "steam", "flatpak"
|
SourceRole, // "desktop", "waydroid", "steam", "flatpak"
|
||||||
StorageIdRole, // .desktop file name or launch URI
|
StorageIdRole, // .desktop file name or launch URI
|
||||||
LaunchCommandRole,
|
LaunchCommandRole,
|
||||||
|
LaunchMethodRole,
|
||||||
ArtworkRole, // path to banner/grid image (empty if none)
|
ArtworkRole, // path to banner/grid image (empty if none)
|
||||||
|
LastPlayedTextRole,
|
||||||
InstalledRole,
|
InstalledRole,
|
||||||
|
PinnedRole,
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
|
|
@ -54,6 +61,9 @@ public:
|
||||||
void setSourceFilter(const QString &source);
|
void setSourceFilter(const QString &source);
|
||||||
bool overlayEnabled() const;
|
bool overlayEnabled() const;
|
||||||
void setOverlayEnabled(bool enabled);
|
void setOverlayEnabled(bool enabled);
|
||||||
|
bool mangohudAvailable() const;
|
||||||
|
int fpsLimit() const;
|
||||||
|
void setFpsLimit(int limit);
|
||||||
bool launchPending() const;
|
bool launchPending() const;
|
||||||
QString pendingLaunchName() const;
|
QString pendingLaunchName() const;
|
||||||
QString lastLaunchError() const;
|
QString lastLaunchError() const;
|
||||||
|
|
@ -61,9 +71,17 @@ public:
|
||||||
Q_INVOKABLE void refresh();
|
Q_INVOKABLE void refresh();
|
||||||
Q_INVOKABLE void launch(int index);
|
Q_INVOKABLE void launch(int index);
|
||||||
Q_INVOKABLE void launchByStorageId(const QString &storageId);
|
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 QVariantList recentGames(int limit = 5) const;
|
||||||
Q_INVOKABLE void clearPendingLaunch();
|
Q_INVOKABLE void clearPendingLaunch();
|
||||||
Q_INVOKABLE void clearLastLaunchError();
|
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:
|
Q_SIGNALS:
|
||||||
void countChanged();
|
void countChanged();
|
||||||
|
|
@ -71,10 +89,13 @@ Q_SIGNALS:
|
||||||
void filterStringChanged();
|
void filterStringChanged();
|
||||||
void sourceFilterChanged();
|
void sourceFilterChanged();
|
||||||
void overlayEnabledChanged();
|
void overlayEnabledChanged();
|
||||||
|
void mangohudAvailableChanged();
|
||||||
|
void fpsLimitChanged();
|
||||||
void launchPendingChanged();
|
void launchPendingChanged();
|
||||||
void lastLaunchErrorChanged();
|
void lastLaunchErrorChanged();
|
||||||
void gameLaunched(const QString &name);
|
void gameLaunched(const QString &name);
|
||||||
void gameLaunchFailed(const QString &name, const QString &error);
|
void gameLaunchFailed(const QString &name, const QString &error);
|
||||||
|
void recentGamesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct GameEntry {
|
struct GameEntry {
|
||||||
|
|
@ -98,12 +119,16 @@ private:
|
||||||
void saveRecentTimestamp(const QString &storageId, const QDateTime &when);
|
void saveRecentTimestamp(const QString &storageId, const QDateTime &when);
|
||||||
void applyFilter();
|
void applyFilter();
|
||||||
void launchEntry(GameEntry &entry);
|
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.
|
// Returns the current m_allGames index for the storage id.
|
||||||
// Callers must re-lookup after any mutation that can rebuild or reorder the list.
|
// Callers must re-lookup after any mutation that can rebuild or reorder the list.
|
||||||
int findEntryIndexByStorageId(const QString &storageId) const;
|
int findEntryIndexByStorageId(const QString &storageId) const;
|
||||||
void markLaunchSucceeded(const QString &storageId, const QString &name);
|
void markLaunchSucceeded(const QString &storageId, const QString &name);
|
||||||
void markLaunchFailed(const QString &name, const QString &error);
|
void markLaunchFailed(const QString &name, const QString &error);
|
||||||
void setPendingLaunch(const QString &name);
|
void setPendingLaunch(const QString &name);
|
||||||
|
void loadPinnedGames();
|
||||||
|
|
||||||
QList<GameEntry> m_allGames;
|
QList<GameEntry> m_allGames;
|
||||||
QList<GameEntry> m_games; // filtered view
|
QList<GameEntry> m_games; // filtered view
|
||||||
|
|
@ -112,6 +137,10 @@ private:
|
||||||
KSharedConfigPtr m_config;
|
KSharedConfigPtr m_config;
|
||||||
bool m_loading = false;
|
bool m_loading = false;
|
||||||
bool m_overlayEnabled = false;
|
bool m_overlayEnabled = false;
|
||||||
|
int m_fpsLimit = 0;
|
||||||
|
bool m_mangohudAvailable = false;
|
||||||
|
QString m_mangohudPath;
|
||||||
|
QSet<QString> m_pinnedGames;
|
||||||
bool m_launchPending = false;
|
bool m_launchPending = false;
|
||||||
QString m_pendingLaunchName;
|
QString m_pendingLaunchName;
|
||||||
QString m_lastLaunchError;
|
QString m_lastLaunchError;
|
||||||
|
|
|
||||||
|
|
@ -505,7 +505,45 @@ Item {
|
||||||
id: overlaySwitch
|
id: overlaySwitch
|
||||||
text: i18n("Perf Overlay")
|
text: i18n("Perf Overlay")
|
||||||
checked: GamingShell.GameLauncherProvider.overlayEnabled
|
checked: GamingShell.GameLauncherProvider.overlayEnabled
|
||||||
|
enabled: GamingShell.GameLauncherProvider.mangohudAvailable
|
||||||
|
opacity: enabled ? 1.0 : 0.5
|
||||||
onToggled: GamingShell.GameLauncherProvider.overlayEnabled = checked
|
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)
|
// GameMode status (auto-managed, read-only indicator)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue