mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 15:03:09 +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 <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLocale>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
|
|
@ -28,6 +30,8 @@
|
|||
#include <memory>
|
||||
|
||||
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_gameShellPackagesKey = QStringLiteral("gameShellPackages");
|
||||
|
||||
|
|
@ -266,6 +270,9 @@ GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
|||
m_pendingLaunchTimer.setInterval(15000);
|
||||
m_pendingLaunchTimer.setSingleShot(true);
|
||||
connect(&m_pendingLaunchTimer, &QTimer::timeout, this, &GameLauncherProvider::clearPendingLaunch);
|
||||
m_mangohudPath = QStandardPaths::findExecutable(QStringLiteral("mangohud"));
|
||||
m_mangohudAvailable = !m_mangohudPath.isEmpty();
|
||||
loadPinnedGames();
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
|
@ -291,10 +298,16 @@ QVariant GameLauncherProvider::data(const QModelIndex &index, int role) const
|
|||
return g.storageId;
|
||||
case LaunchCommandRole:
|
||||
return g.launchCommand;
|
||||
case LaunchMethodRole:
|
||||
return launchMethodForEntry(g);
|
||||
case ArtworkRole:
|
||||
return g.artwork;
|
||||
case LastPlayedTextRole:
|
||||
return formatLastPlayed(g.lastPlayed);
|
||||
case InstalledRole:
|
||||
return g.installed;
|
||||
case PinnedRole:
|
||||
return m_pinnedGames.contains(g.storageId);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
@ -307,8 +320,11 @@ QHash<int, QByteArray> GameLauncherProvider::roleNames() const
|
|||
{SourceRole, "source"},
|
||||
{StorageIdRole, "storageId"},
|
||||
{LaunchCommandRole, "launchCommand"},
|
||||
{LaunchMethodRole, "launchMethod"},
|
||||
{ArtworkRole, "artwork"},
|
||||
{LastPlayedTextRole, "lastPlayedText"},
|
||||
{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)
|
||||
{
|
||||
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")) {
|
||||
auto service = KService::serviceByStorageId(entry.storageId);
|
||||
if (!service) {
|
||||
|
|
@ -402,6 +476,27 @@ void GameLauncherProvider::launchEntry(GameEntry &entry)
|
|||
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);
|
||||
connect(job, &KJob::result, this, [this, job, storageId = entry.storageId, name = entry.name]() {
|
||||
if (job->error() != 0) {
|
||||
|
|
@ -426,7 +521,13 @@ void GameLauncherProvider::launchEntry(GameEntry &entry)
|
|||
}
|
||||
|
||||
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));
|
||||
return;
|
||||
}
|
||||
|
|
@ -818,6 +919,46 @@ bool GameLauncherProvider::overlayEnabled() const
|
|||
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)
|
||||
{
|
||||
if (m_overlayEnabled == enabled) {
|
||||
|
|
@ -825,15 +966,6 @@ void GameLauncherProvider::setOverlayEnabled(bool enabled)
|
|||
}
|
||||
m_overlayEnabled = enabled;
|
||||
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()
|
||||
|
|
@ -851,6 +983,10 @@ void GameLauncherProvider::applyFilter()
|
|||
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();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
|
@ -873,6 +1009,129 @@ void GameLauncherProvider::saveRecentTimestamp(const QString &storageId, const Q
|
|||
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()
|
||||
{
|
||||
if (!m_launchPending && m_pendingLaunchName.isEmpty()) {
|
||||
|
|
@ -917,6 +1176,7 @@ void GameLauncherProvider::markLaunchSucceeded(const QString &storageId, const Q
|
|||
|
||||
setPendingLaunch(name);
|
||||
Q_EMIT gameLaunched(name);
|
||||
Q_EMIT recentGamesChanged();
|
||||
}
|
||||
|
||||
void GameLauncherProvider::markLaunchFailed(const QString &name, const QString &error)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVariantMap>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
#include <KConfigWatcher>
|
||||
|
|
@ -24,6 +26,8 @@ class GameLauncherProvider : public QAbstractListModel
|
|||
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)
|
||||
|
|
@ -37,8 +41,11 @@ public:
|
|||
SourceRole, // "desktop", "waydroid", "steam", "flatpak"
|
||||
StorageIdRole, // .desktop file name or launch URI
|
||||
LaunchCommandRole,
|
||||
LaunchMethodRole,
|
||||
ArtworkRole, // path to banner/grid image (empty if none)
|
||||
LastPlayedTextRole,
|
||||
InstalledRole,
|
||||
PinnedRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
|
|
@ -54,6 +61,9 @@ public:
|
|||
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;
|
||||
|
|
@ -61,9 +71,17 @@ public:
|
|||
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();
|
||||
|
|
@ -71,10 +89,13 @@ Q_SIGNALS:
|
|||
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 {
|
||||
|
|
@ -98,12 +119,16 @@ private:
|
|||
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
|
||||
|
|
@ -112,6 +137,10 @@ private:
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -505,7 +505,45 @@ Item {
|
|||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue