mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-28 22:53:09 +00:00
Track launch state in Game Center
Record recent launches only after the launcher reports success instead of optimistically on every attempt. For desktop entries, wait for the ApplicationLauncherJob result; for detached commands, use startDetached's return value and show an inline error when startup fails. Parse Steam libraryfolders and app manifests as KeyValues instead of matching individual lines. This makes Steam discovery less brittle when the files contain comments, nested blocks, or different formatting. Keep Continue Playing visible alongside Running and show a temporary launching state so the overlay does not pretend a task already exists. Rename the GameMode status text to say what the shell actually knows.
This commit is contained in:
parent
d913c2ab0a
commit
4dd0e17afb
5 changed files with 419 additions and 49 deletions
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <KConfigGroup>
|
||||
#include <KIO/ApplicationLauncherJob>
|
||||
#include <KJob>
|
||||
#include <KService>
|
||||
#include <KSharedConfig>
|
||||
#include <KShell>
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -23,13 +25,203 @@
|
|||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <memory>
|
||||
|
||||
static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed");
|
||||
|
||||
namespace
|
||||
{
|
||||
struct VdfNode {
|
||||
QHash<QString, QString> values;
|
||||
QHash<QString, std::shared_ptr<VdfNode>> children;
|
||||
};
|
||||
|
||||
class VdfTokenizer
|
||||
{
|
||||
public:
|
||||
enum class TokenType {
|
||||
End,
|
||||
String,
|
||||
OpenBrace,
|
||||
CloseBrace,
|
||||
Invalid,
|
||||
};
|
||||
|
||||
struct Token {
|
||||
TokenType type = TokenType::End;
|
||||
QString text;
|
||||
};
|
||||
|
||||
explicit VdfTokenizer(QStringView input)
|
||||
: m_input(input)
|
||||
{
|
||||
}
|
||||
|
||||
Token next()
|
||||
{
|
||||
skipWhitespaceAndComments();
|
||||
|
||||
if (m_pos >= m_input.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QChar current = m_input.at(m_pos);
|
||||
if (current == QLatin1Char('{')) {
|
||||
++m_pos;
|
||||
return {TokenType::OpenBrace, {}};
|
||||
}
|
||||
if (current == QLatin1Char('}')) {
|
||||
++m_pos;
|
||||
return {TokenType::CloseBrace, {}};
|
||||
}
|
||||
if (current == QLatin1Char('"')) {
|
||||
return {TokenType::String, readQuotedString()};
|
||||
}
|
||||
|
||||
return {TokenType::String, readBareString()};
|
||||
}
|
||||
|
||||
int position() const
|
||||
{
|
||||
return m_pos;
|
||||
}
|
||||
|
||||
private:
|
||||
void skipWhitespaceAndComments()
|
||||
{
|
||||
while (m_pos < m_input.size()) {
|
||||
const QChar current = m_input.at(m_pos);
|
||||
if (current.isSpace()) {
|
||||
++m_pos;
|
||||
continue;
|
||||
}
|
||||
if (current == QLatin1Char('/') && m_pos + 1 < m_input.size() && m_input.at(m_pos + 1) == QLatin1Char('/')) {
|
||||
m_pos += 2;
|
||||
while (m_pos < m_input.size() && m_input.at(m_pos) != QLatin1Char('\n')) {
|
||||
++m_pos;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QString readQuotedString()
|
||||
{
|
||||
QString result;
|
||||
++m_pos;
|
||||
|
||||
while (m_pos < m_input.size()) {
|
||||
const QChar current = m_input.at(m_pos++);
|
||||
if (current == QLatin1Char('"')) {
|
||||
return result;
|
||||
}
|
||||
if (current == QLatin1Char('\\') && m_pos < m_input.size()) {
|
||||
const QChar escaped = m_input.at(m_pos++);
|
||||
switch (escaped.unicode()) {
|
||||
case 'n':
|
||||
result.append(QLatin1Char('\n'));
|
||||
break;
|
||||
case 't':
|
||||
result.append(QLatin1Char('\t'));
|
||||
break;
|
||||
case 'r':
|
||||
result.append(QLatin1Char('\r'));
|
||||
break;
|
||||
case '\\':
|
||||
case '"':
|
||||
result.append(escaped);
|
||||
break;
|
||||
default:
|
||||
result.append(escaped);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
result.append(current);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString readBareString()
|
||||
{
|
||||
const int start = m_pos;
|
||||
while (m_pos < m_input.size()) {
|
||||
const QChar current = m_input.at(m_pos);
|
||||
if (current.isSpace() || current == QLatin1Char('{') || current == QLatin1Char('}') || current == QLatin1Char('"')) {
|
||||
break;
|
||||
}
|
||||
if (current == QLatin1Char('/') && m_pos + 1 < m_input.size() && m_input.at(m_pos + 1) == QLatin1Char('/')) {
|
||||
break;
|
||||
}
|
||||
++m_pos;
|
||||
}
|
||||
return m_input.sliced(start, m_pos - start).toString();
|
||||
}
|
||||
|
||||
QStringView m_input;
|
||||
int m_pos = 0;
|
||||
};
|
||||
|
||||
bool parseVdf(const QString &input, VdfNode &root, QString *error)
|
||||
{
|
||||
VdfTokenizer tokenizer(input);
|
||||
QList<VdfNode *> stack = {&root};
|
||||
|
||||
while (true) {
|
||||
const auto key = tokenizer.next();
|
||||
if (key.type == VdfTokenizer::TokenType::End) {
|
||||
if (stack.size() != 1 && error) {
|
||||
*error = QStringLiteral("unexpected end of file");
|
||||
}
|
||||
return stack.size() == 1;
|
||||
}
|
||||
if (key.type == VdfTokenizer::TokenType::CloseBrace) {
|
||||
if (stack.size() == 1) {
|
||||
if (error) {
|
||||
*error = QStringLiteral("unexpected closing brace at position %1").arg(tokenizer.position());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
stack.removeLast();
|
||||
continue;
|
||||
}
|
||||
if (key.type != VdfTokenizer::TokenType::String || key.text.isEmpty()) {
|
||||
if (error) {
|
||||
*error = QStringLiteral("invalid key at position %1").arg(tokenizer.position());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto value = tokenizer.next();
|
||||
if (value.type == VdfTokenizer::TokenType::String) {
|
||||
stack.last()->values.insert(key.text, value.text);
|
||||
continue;
|
||||
}
|
||||
if (value.type == VdfTokenizer::TokenType::OpenBrace) {
|
||||
auto child = std::make_shared<VdfNode>();
|
||||
stack.last()->children.insert(key.text, child);
|
||||
stack.append(child.get());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
*error = QStringLiteral("expected value for key '%1'").arg(key.text);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_config(KSharedConfig::openConfig(QStringLiteral("plasmamobilerc")))
|
||||
{
|
||||
connect(KSycoca::self(), &KSycoca::databaseChanged, this, &GameLauncherProvider::refresh);
|
||||
m_pendingLaunchTimer.setInterval(15000);
|
||||
m_pendingLaunchTimer.setSingleShot(true);
|
||||
connect(&m_pendingLaunchTimer, &QTimer::timeout, this, &GameLauncherProvider::clearPendingLaunch);
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +278,21 @@ bool GameLauncherProvider::loading() const
|
|||
return m_loading;
|
||||
}
|
||||
|
||||
bool GameLauncherProvider::launchPending() const
|
||||
{
|
||||
return m_launchPending;
|
||||
}
|
||||
|
||||
QString GameLauncherProvider::pendingLaunchName() const
|
||||
{
|
||||
return m_pendingLaunchName;
|
||||
}
|
||||
|
||||
QString GameLauncherProvider::lastLaunchError() const
|
||||
{
|
||||
return m_lastLaunchError;
|
||||
}
|
||||
|
||||
void GameLauncherProvider::refresh()
|
||||
{
|
||||
m_loading = true;
|
||||
|
|
@ -142,27 +349,45 @@ void GameLauncherProvider::launchByStorageId(const QString &storageId)
|
|||
|
||||
void GameLauncherProvider::launchEntry(GameEntry &entry)
|
||||
{
|
||||
clearLastLaunchError();
|
||||
|
||||
if (entry.source == QLatin1String("desktop")) {
|
||||
auto service = KService::serviceByStorageId(entry.storageId);
|
||||
if (service) {
|
||||
auto *job = new KIO::ApplicationLauncherJob(service);
|
||||
job->start();
|
||||
if (!service) {
|
||||
markLaunchFailed(entry.name, QStringLiteral("Desktop entry is no longer available"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto *job = new KIO::ApplicationLauncherJob(service);
|
||||
connect(job, &KJob::result, this, [this, job, storageId = entry.storageId, name = entry.name]() {
|
||||
if (job->error() != 0) {
|
||||
markLaunchFailed(name, job->errorString());
|
||||
return;
|
||||
}
|
||||
markLaunchSucceeded(storageId, name);
|
||||
});
|
||||
job->start();
|
||||
} else if (entry.launchCommand.contains(QStringLiteral("://"))) {
|
||||
// Protocol handler (e.g. heroic://launch/...) — open via xdg-open
|
||||
QProcess::startDetached(QStringLiteral("xdg-open"), {entry.launchCommand});
|
||||
if (!QProcess::startDetached(QStringLiteral("xdg-open"), {entry.launchCommand})) {
|
||||
markLaunchFailed(entry.name, QStringLiteral("Unable to start xdg-open"));
|
||||
return;
|
||||
}
|
||||
markLaunchSucceeded(entry.storageId, entry.name);
|
||||
} else {
|
||||
QStringList parts = KShell::splitArgs(entry.launchCommand);
|
||||
if (!parts.isEmpty()) {
|
||||
QString program = parts.takeFirst();
|
||||
QProcess::startDetached(program, parts);
|
||||
if (parts.isEmpty()) {
|
||||
markLaunchFailed(entry.name, QStringLiteral("Launch command is empty"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT gameLaunched(entry.name);
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
saveRecentTimestamp(entry.storageId, now);
|
||||
entry.lastPlayed = now;
|
||||
QString program = parts.takeFirst();
|
||||
if (!QProcess::startDetached(program, parts)) {
|
||||
markLaunchFailed(entry.name, QStringLiteral("Unable to start %1").arg(program));
|
||||
return;
|
||||
}
|
||||
markLaunchSucceeded(entry.storageId, entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
void GameLauncherProvider::deduplicateGames()
|
||||
|
|
@ -237,21 +462,30 @@ void GameLauncherProvider::loadSteamGames()
|
|||
if (!vdf.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
// Simple parse: look for "path" lines
|
||||
static const QRegularExpression pathRe(QStringLiteral("\"path\"\\s+\"([^\"]+)\""));
|
||||
QTextStream stream(&vdf);
|
||||
while (!stream.atEnd()) {
|
||||
const QString line = stream.readLine();
|
||||
auto match = pathRe.match(line);
|
||||
if (match.hasMatch()) {
|
||||
libraryPaths.append(match.captured(1));
|
||||
const QString content = QString::fromUtf8(vdf.readAll());
|
||||
VdfNode document;
|
||||
QString error;
|
||||
if (!parseVdf(content, document, &error)) {
|
||||
qWarning() << "GameLauncherProvider: cannot parse Steam libraryfolders" << vdfPath << error;
|
||||
continue;
|
||||
}
|
||||
|
||||
const VdfNode *libraries = nullptr;
|
||||
if (document.children.contains(QStringLiteral("libraryfolders"))) {
|
||||
libraries = document.children.value(QStringLiteral("libraryfolders")).get();
|
||||
} else {
|
||||
libraries = &document;
|
||||
}
|
||||
|
||||
for (auto it = libraries->children.cbegin(); it != libraries->children.cend(); ++it) {
|
||||
const QString path = it.value()->values.value(QStringLiteral("path"));
|
||||
if (!path.isEmpty()) {
|
||||
libraryPaths.append(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan each library path for appmanifest_*.acf
|
||||
static const QRegularExpression nameRe(QStringLiteral("\"name\"\\s+\"([^\"]+)\""));
|
||||
static const QRegularExpression appidRe(QStringLiteral("\"appid\"\\s+\"(\\d+)\""));
|
||||
libraryPaths.removeDuplicates();
|
||||
|
||||
for (const auto &libPath : std::as_const(libraryPaths)) {
|
||||
QDir steamapps(libPath + QStringLiteral("/steamapps"));
|
||||
|
|
@ -264,28 +498,19 @@ void GameLauncherProvider::loadSteamGames()
|
|||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
QString appName;
|
||||
QString appId;
|
||||
QTextStream ts(&f);
|
||||
while (!ts.atEnd()) {
|
||||
const QString line = ts.readLine();
|
||||
if (appName.isEmpty()) {
|
||||
auto m = nameRe.match(line);
|
||||
if (m.hasMatch()) {
|
||||
appName = m.captured(1);
|
||||
}
|
||||
}
|
||||
if (appId.isEmpty()) {
|
||||
auto m = appidRe.match(line);
|
||||
if (m.hasMatch()) {
|
||||
appId = m.captured(1);
|
||||
}
|
||||
}
|
||||
if (!appName.isEmpty() && !appId.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
VdfNode manifestData;
|
||||
QString error;
|
||||
if (!parseVdf(QString::fromUtf8(f.readAll()), manifestData, &error)) {
|
||||
qWarning() << "GameLauncherProvider: cannot parse Steam manifest" << manifest << error;
|
||||
continue;
|
||||
}
|
||||
|
||||
const VdfNode *appState =
|
||||
manifestData.children.contains(QStringLiteral("AppState")) ? manifestData.children.value(QStringLiteral("AppState")).get() : &manifestData;
|
||||
|
||||
const QString appName = appState->values.value(QStringLiteral("name"));
|
||||
const QString appId = appState->values.value(QStringLiteral("appid"));
|
||||
|
||||
if (appName.isEmpty() || appId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -593,6 +818,76 @@ void GameLauncherProvider::saveRecentTimestamp(const QString &storageId, const Q
|
|||
group.sync();
|
||||
}
|
||||
|
||||
void GameLauncherProvider::clearPendingLaunch()
|
||||
{
|
||||
if (!m_launchPending && m_pendingLaunchName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingLaunchTimer.stop();
|
||||
m_launchPending = false;
|
||||
m_pendingLaunchName.clear();
|
||||
Q_EMIT launchPendingChanged();
|
||||
}
|
||||
|
||||
void GameLauncherProvider::clearLastLaunchError()
|
||||
{
|
||||
if (m_lastLaunchError.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastLaunchError.clear();
|
||||
Q_EMIT lastLaunchErrorChanged();
|
||||
}
|
||||
|
||||
GameLauncherProvider::GameEntry *GameLauncherProvider::findEntryByStorageId(const QString &storageId)
|
||||
{
|
||||
for (auto &entry : m_allGames) {
|
||||
if (entry.storageId == storageId) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void GameLauncherProvider::markLaunchSucceeded(const QString &storageId, const QString &name)
|
||||
{
|
||||
if (auto *entry = findEntryByStorageId(storageId)) {
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
saveRecentTimestamp(entry->storageId, now);
|
||||
entry->lastPlayed = now;
|
||||
}
|
||||
|
||||
setPendingLaunch(name);
|
||||
Q_EMIT gameLaunched(name);
|
||||
}
|
||||
|
||||
void GameLauncherProvider::markLaunchFailed(const QString &name, const QString &error)
|
||||
{
|
||||
clearPendingLaunch();
|
||||
|
||||
const QString message = error.isEmpty() ? tr("Unable to launch %1").arg(name) : tr("Unable to launch %1: %2").arg(name, error);
|
||||
|
||||
if (m_lastLaunchError != message) {
|
||||
m_lastLaunchError = message;
|
||||
Q_EMIT lastLaunchErrorChanged();
|
||||
}
|
||||
|
||||
Q_EMIT gameLaunchFailed(name, message);
|
||||
}
|
||||
|
||||
void GameLauncherProvider::setPendingLaunch(const QString &name)
|
||||
{
|
||||
const bool changed = !m_launchPending || m_pendingLaunchName != name;
|
||||
m_launchPending = true;
|
||||
m_pendingLaunchName = name;
|
||||
m_pendingLaunchTimer.start();
|
||||
|
||||
if (changed) {
|
||||
Q_EMIT launchPendingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QVariantList GameLauncherProvider::recentGames(int limit) const
|
||||
{
|
||||
// Gather entries that have been launched at least once
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
#include <KSharedConfig>
|
||||
|
|
@ -22,6 +23,9 @@ 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 launchPending READ launchPending NOTIFY launchPendingChanged)
|
||||
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
|
||||
Q_PROPERTY(QString lastLaunchError READ lastLaunchError NOTIFY lastLaunchErrorChanged)
|
||||
|
||||
public:
|
||||
explicit GameLauncherProvider(QObject *parent = nullptr);
|
||||
|
|
@ -49,11 +53,16 @@ public:
|
|||
void setSourceFilter(const QString &source);
|
||||
bool overlayEnabled() const;
|
||||
void setOverlayEnabled(bool enabled);
|
||||
bool launchPending() const;
|
||||
QString pendingLaunchName() const;
|
||||
QString lastLaunchError() const;
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
Q_INVOKABLE void launch(int index);
|
||||
Q_INVOKABLE void launchByStorageId(const QString &storageId);
|
||||
Q_INVOKABLE QVariantList recentGames(int limit = 5) const;
|
||||
Q_INVOKABLE void clearPendingLaunch();
|
||||
Q_INVOKABLE void clearLastLaunchError();
|
||||
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
|
|
@ -61,7 +70,10 @@ Q_SIGNALS:
|
|||
void filterStringChanged();
|
||||
void sourceFilterChanged();
|
||||
void overlayEnabledChanged();
|
||||
void launchPendingChanged();
|
||||
void lastLaunchErrorChanged();
|
||||
void gameLaunched(const QString &name);
|
||||
void gameLaunchFailed(const QString &name, const QString &error);
|
||||
|
||||
private:
|
||||
struct GameEntry {
|
||||
|
|
@ -85,6 +97,10 @@ private:
|
|||
void saveRecentTimestamp(const QString &storageId, const QDateTime &when);
|
||||
void applyFilter();
|
||||
void launchEntry(GameEntry &entry);
|
||||
GameEntry *findEntryByStorageId(const QString &storageId);
|
||||
void markLaunchSucceeded(const QString &storageId, const QString &name);
|
||||
void markLaunchFailed(const QString &name, const QString &error);
|
||||
void setPendingLaunch(const QString &name);
|
||||
|
||||
QList<GameEntry> m_allGames;
|
||||
QList<GameEntry> m_games; // filtered view
|
||||
|
|
@ -93,4 +109,8 @@ private:
|
|||
KSharedConfigPtr m_config;
|
||||
bool m_loading = false;
|
||||
bool m_overlayEnabled = false;
|
||||
bool m_launchPending = false;
|
||||
QString m_pendingLaunchName;
|
||||
QString m_lastLaunchError;
|
||||
QTimer m_pendingLaunchTimer;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Window {
|
|||
id: root
|
||||
|
||||
required property var folio
|
||||
property int launchTaskBaseline: 0
|
||||
|
||||
signal gameStarted()
|
||||
signal dismissRequested()
|
||||
|
|
@ -51,14 +52,14 @@ Window {
|
|||
|
||||
function launchGame(index) {
|
||||
pulsePrimaryGamepad(14000, 22000, 80)
|
||||
launchTaskBaseline = runningGames.taskCount
|
||||
GamingShell.GameLauncherProvider.launch(index)
|
||||
launchFade.restart()
|
||||
}
|
||||
|
||||
function launchGameByStorageId(storageId) {
|
||||
pulsePrimaryGamepad(14000, 22000, 80)
|
||||
launchTaskBaseline = runningGames.taskCount
|
||||
GamingShell.GameLauncherProvider.launchByStorageId(storageId)
|
||||
launchFade.restart()
|
||||
}
|
||||
|
||||
width: Screen.width
|
||||
|
|
@ -79,6 +80,25 @@ Window {
|
|||
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GamingShell.GameLauncherProvider
|
||||
|
||||
function onGameLaunched(name) {
|
||||
launchFade.restart()
|
||||
}
|
||||
|
||||
function onGameLaunchFailed(name, error) {
|
||||
launchErrorTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: launchErrorTimer
|
||||
interval: 6000
|
||||
repeat: false
|
||||
onTriggered: GamingShell.GameLauncherProvider.clearLastLaunchError()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
GamingShell.GameLauncherProvider.filterString = ""
|
||||
|
|
@ -376,15 +396,49 @@ Window {
|
|||
RunningGamesView {
|
||||
id: runningGames
|
||||
Layout.fillWidth: true
|
||||
onTaskActivated: root.gameStarted()
|
||||
onTaskActivated: {
|
||||
GamingShell.GameLauncherProvider.clearPendingLaunch()
|
||||
root.gameStarted()
|
||||
}
|
||||
onMoveDownRequested: grid.forceActiveFocus()
|
||||
onTaskCountChanged: {
|
||||
if (GamingShell.GameLauncherProvider.launchPending
|
||||
&& taskCount > root.launchTaskBaseline) {
|
||||
GamingShell.GameLauncherProvider.clearPendingLaunch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
Layout.fillWidth: true
|
||||
type: Kirigami.MessageType.Error
|
||||
text: GamingShell.GameLauncherProvider.lastLaunchError
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
visible: GamingShell.GameLauncherProvider.launchPending
|
||||
|
||||
Kirigami.Icon {
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
source: "system-run"
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18n("Launching %1…", GamingShell.GameLauncherProvider.pendingLaunchName)
|
||||
opacity: 0.75
|
||||
}
|
||||
}
|
||||
|
||||
// ---- continue playing ----
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
visible: recentList.count > 0 && !runningGames.hasTasks
|
||||
visible: recentList.count > 0
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
|
|
|
|||
|
|
@ -520,8 +520,8 @@ Item {
|
|||
}
|
||||
PC3.Label {
|
||||
text: GamingShell.GameModeControl.active
|
||||
? i18n("GameMode active")
|
||||
: i18n("GameMode standby")
|
||||
? i18n("GameMode requested")
|
||||
: i18n("GameMode not requested")
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Item {
|
|||
|
||||
implicitHeight: taskList.count > 0 ? column.implicitHeight : 0
|
||||
readonly property bool hasTasks: taskList.count > 0
|
||||
readonly property int taskCount: taskList.count
|
||||
|
||||
signal taskActivated()
|
||||
signal moveDownRequested()
|
||||
|
|
|
|||
Loading…
Reference in a new issue