Compare commits

..

No commits in common. "master" and "v0.1.1rc2" have entirely different histories.

27 changed files with 246 additions and 2784 deletions

View file

@ -6,19 +6,6 @@
A unified game launcher for KDE Plasma 6, inspired by [KDE Apps Initiative issue#26](https://invent.kde.org/teams/apps-initiative/tasks/-/issues/26).
## Project tracks
This repository currently has two development tracks:
- **Legacy (launcher-first)**
- The current `master`/`legacy` baseline: a traditional game launcher UI.
- See `docs/LEGACY.md`.
- **Gamecenter (launcher + daemons)**
- The `gamecenter` branch: an experimental direction to evolve A-La-Karte into a launcher + game center suitable for a console-like KDE experience.
- See `docs/GAMECENTER.md`.
For documentation index, see `docs/README.md`.
## Features
- Import games from Steam, Lutris, Heroic, Bottles, itch.io, Legendary, RetroArch playlists, Flatpak applications, and desktop entries

View file

@ -1,52 +0,0 @@
# Gamecenter track (Launcher + Game Center)
## Goal
The gamecenter track evolves A-La-Karte from “a launcher” into “a launcher + game center” suitable for a **fully integrated, console-like experience on KDE**.
Key ideas:
- The UI becomes a client of background services.
- Launching and monitoring games is handled by a daemon.
- Runner resolution (Wine/Proton) is handled by a runner manager service.
## Architecture (high level)
### UI (A-La-Karte)
- Displays the library and settings.
- Requests launches via the Game Center daemon instead of launching processes directly.
- Surfaces daemon/service errors to the user.
### Game Center daemon
- Manages game sessions (launch/stop, running state, concurrency policy).
- Tracks/monitors sessions so Steam/Lutris URL-based launches can still be managed.
- Intended to be activatable via DBus/systemd.
DBus service (planned/implemented on the gamecenter branch):
- `org.kde.GameCenter1`
### Runner manager daemon
- Manages “runners” (e.g. Proton, Wine, GE builds):
- install/uninstall
- inventory listing
- resolve launch specs deterministically
- prefix lifecycle helpers
DBus service (planned/implemented on the gamecenter branch):
- `org.kde.ALaKarte.Runner1`
## Branches
- `master` / `legacy`
- Launcher-focused baseline.
- `gamecenter`
- Experimental daemon-based direction.
## Status
This document is intentionally a living overview. Detailed API docs / roadmaps should live in separate documents as they solidify.

View file

@ -1,18 +0,0 @@
# Legacy track (Launcher)
## What it is
The legacy track is a traditional desktop application:
- The UI directly owns the game library.
- The UI launches games.
- Focus is on being a high-quality KDE Plasma launcher with good keyboard/gamepad navigation.
## What it is not
- A system-level game session manager.
- A daemon that monitors and manages sessions across launchers.
## Why it exists
This track is the stable baseline and the fastest way to iterate on UX, importers, and per-game configuration.

View file

@ -1,14 +0,0 @@
# Documentation
This folder contains higher-level project documentation.
## Overview
- `LEGACY.md`
- Notes on the current launcher-focused architecture (historically the "legacy" track).
- `GAMECENTER.md`
- Notes on the "gamecenter" track: a launcher + Game Center daemon-based architecture for a more console-like KDE experience.
## Contributing docs
If you add a new document, link it from this index so it stays discoverable.

View file

@ -1,22 +0,0 @@
# Roadmap
This document captures the high-level direction of the project.
## Legacy track (launcher-first)
- Maintain a stable, polished KDE Plasma launcher.
- Keep importers and library UX solid and reliable.
## Gamecenter track (launcher + daemons)
- Evolve into a fully integrated, console-like experience on KDE.
- Move launching + session monitoring into a daemon.
- Add runner management (Wine/Proton) as a service.
## Near-term goals
- (Fill in)
## Longer-term goals
- (Fill in)

View file

@ -8,7 +8,6 @@ add_executable(alakarte
gamesortfiltermodel.cpp
game.cpp
gamepadmanager.cpp
inputmanager.cpp
mediamanager.cpp
screenshotmodel.cpp
platformimporter.cpp
@ -64,7 +63,6 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
qml/LibraryView.qml
qml/GameCard.qml
qml/GameDetailsSheet.qml
qml/DiagnosticsSheet.qml
qml/SettingsPage.qml
qml/SidebarView.qml
qml/GameEditDialog.qml
@ -120,7 +118,6 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
gamesortfiltermodel.h
game.h
gamepadmanager.h
inputmanager.h
platformimporter.h
steamimporter.h
lutrisimporter.h

View file

@ -18,9 +18,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QSet>
#include <QStandardPaths>
#include <QTimer>
#include <QtConcurrent>
App *App::s_instance = nullptr;
@ -144,12 +142,6 @@ App::App(QObject *parent)
}
}
});
if (m_config->autoImportOnStartup()) {
QTimer::singleShot(0, this, [this]() {
importAllGames();
});
}
}
App *App::instance()
@ -223,17 +215,18 @@ void App::importAllGames()
if (m_importing)
return;
const bool doSteam = m_config->importSteam();
const bool doLutris = m_config->importLutris();
const bool doHeroic = m_config->importHeroic();
const bool doDesktop = m_config->importDesktop();
const bool doBottles = m_config->importBottles();
const bool doFlatpak = m_config->importFlatpak();
const bool doItch = m_config->importItch();
const bool doLegendary = m_config->importLegendary();
const bool doRetroArch = m_config->importRetroArch();
const bool anyEnabled = m_config->importSteam() || m_config->importLutris() || m_config->importHeroic() || m_config->importDesktop()
|| m_config->importBottles() || m_config->importFlatpak() || m_config->importItch() || m_config->importLegendary() || m_config->importRetroArch();
const bool anyEnabled = doSteam || doLutris || doHeroic || doDesktop || doBottles || doFlatpak || doItch || doLegendary || doRetroArch;
const bool doSteam = m_config->importSteam() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Steam"));
const bool doLutris = m_config->importLutris() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Lutris"));
const bool doHeroic = m_config->importHeroic() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Heroic"));
const bool doDesktop = m_config->importDesktop() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Desktop"));
const bool doBottles = m_config->importBottles() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Bottles"));
const bool doFlatpak = m_config->importFlatpak() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Flatpak"));
const bool doItch = m_config->importItch() && !m_gameModel->hasPlatformPrefix(QStringLiteral("itch.io"));
const bool doLegendary = m_config->importLegendary() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Legendary"));
const bool doRetroArch = m_config->importRetroArch() && !m_gameModel->hasPlatformPrefix(QStringLiteral("RetroArch"));
if (!anyEnabled) {
setImportStatus(tr("No import sources enabled"));
@ -241,55 +234,17 @@ void App::importAllGames()
return;
}
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
if (!(doSteam || doLutris || doHeroic || doDesktop || doBottles || doFlatpak || doItch || doLegendary || doRetroArch)) {
setImportStatus(tr("All enabled sources already imported"));
Q_EMIT importCompleted(0);
return;
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Importing games..."));
[[maybe_unused]] auto future = QtConcurrent::run([this,
doSteam,
doLutris,
doHeroic,
doDesktop,
doBottles,
doFlatpak,
doItch,
doLegendary,
doRetroArch,
existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this, doSteam, doLutris, doHeroic, doDesktop, doBottles, doFlatpak, doItch, doLegendary, doRetroArch]() {
int totalCount = 0;
QSet<QString> seenIds = existingIds;
const auto keepNewGames = [&seenIds](const QList<Game *> &games) {
QList<Game *> result;
result.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
result.append(game);
}
return result;
};
// Import from Steam
if (doSteam) {
@ -301,7 +256,7 @@ void App::importAllGames()
Qt::QueuedConnection);
SteamImporter steamImporter;
QList<Game *> steamGames = keepNewGames(steamImporter.importGames());
QList<Game *> steamGames = steamImporter.importGames();
for (Game *game : steamGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -336,7 +291,7 @@ void App::importAllGames()
Qt::QueuedConnection);
LutrisImporter lutrisImporter;
QList<Game *> lutrisGames = keepNewGames(lutrisImporter.importGames());
QList<Game *> lutrisGames = lutrisImporter.importGames();
for (Game *game : lutrisGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -371,7 +326,7 @@ void App::importAllGames()
Qt::QueuedConnection);
HeroicImporter heroicImporter;
QList<Game *> heroicGames = keepNewGames(heroicImporter.importGames());
QList<Game *> heroicGames = heroicImporter.importGames();
for (Game *game : heroicGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -406,7 +361,7 @@ void App::importAllGames()
Qt::QueuedConnection);
DesktopImporter desktopImporter;
QList<Game *> desktopGames = keepNewGames(desktopImporter.importGames());
QList<Game *> desktopGames = desktopImporter.importGames();
for (Game *game : desktopGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -441,7 +396,7 @@ void App::importAllGames()
Qt::QueuedConnection);
BottlesImporter bottlesImporter;
QList<Game *> bottlesGames = keepNewGames(bottlesImporter.importGames());
QList<Game *> bottlesGames = bottlesImporter.importGames();
for (Game *game : bottlesGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -476,7 +431,7 @@ void App::importAllGames()
Qt::QueuedConnection);
FlatpakImporter flatpakImporter;
QList<Game *> flatpakGames = keepNewGames(flatpakImporter.importGames());
QList<Game *> flatpakGames = flatpakImporter.importGames();
for (Game *game : flatpakGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -511,7 +466,7 @@ void App::importAllGames()
Qt::QueuedConnection);
ItchImporter itchImporter;
QList<Game *> itchGames = keepNewGames(itchImporter.importGames());
QList<Game *> itchGames = itchImporter.importGames();
for (Game *game : itchGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -546,7 +501,7 @@ void App::importAllGames()
Qt::QueuedConnection);
LegendaryImporter legendaryImporter;
QList<Game *> legendaryGames = keepNewGames(legendaryImporter.importGames());
QList<Game *> legendaryGames = legendaryImporter.importGames();
for (Game *game : legendaryGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -581,7 +536,7 @@ void App::importAllGames()
Qt::QueuedConnection);
RetroArchImporter retroArchImporter;
QList<Game *> retroArchGames = keepNewGames(retroArchImporter.importGames());
QList<Game *> retroArchGames = retroArchImporter.importGames();
for (Game *game : retroArchGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
@ -610,7 +565,7 @@ void App::importAllGames()
QMetaObject::invokeMethod(
this,
[this, totalCount]() {
setImportStatus(tr("Import complete: %1 new games found").arg(totalCount));
setImportStatus(tr("Import complete: %1 games found").arg(totalCount));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(totalCount);
@ -624,57 +579,28 @@ void App::importFromSteam()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning Steam library..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
SteamImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("Steam import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("Steam import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -685,57 +611,28 @@ void App::importFromLutris()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning Lutris library..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
LutrisImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("Lutris import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("Lutris import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -746,57 +643,28 @@ void App::importFromHeroic()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning Heroic library..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
HeroicImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("Heroic import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("Heroic import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -807,57 +675,28 @@ void App::importFromDesktop()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning desktop entries..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
DesktopImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("Desktop import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("Desktop import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -868,57 +707,28 @@ void App::importFromBottles()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning Bottles..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
BottlesImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("Bottles import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("Bottles import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -929,57 +739,28 @@ void App::importFromFlatpak()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning Flatpak games..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
FlatpakImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("Flatpak import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("Flatpak import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -990,57 +771,28 @@ void App::importFromItch()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning itch.io library..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
ItchImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("itch.io import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("itch.io import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -1051,57 +803,28 @@ void App::importFromLegendary()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning Legendary library..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
LegendaryImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("Legendary import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("Legendary import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});
@ -1112,57 +835,28 @@ void App::importFromRetroArch()
if (m_importing)
return;
const QSet<QString> existingIds = [this]() {
QSet<QString> ids;
const QList<Game *> games = m_gameModel->allGames();
for (Game *game : games) {
if (game) {
ids.insert(game->id());
}
}
return ids;
}();
setImporting(true);
setImportStatus(tr("Scanning RetroArch playlists..."));
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
RetroArchImporter importer;
QList<Game *> games = importer.importGames();
QSet<QString> seenIds = existingIds;
QList<Game *> newGames;
newGames.reserve(games.size());
for (Game *game : games) {
if (!game) {
continue;
}
const QString id = game->id();
if (id.isEmpty() || seenIds.contains(id)) {
delete game;
continue;
}
seenIds.insert(id);
newGames.append(game);
}
for (Game *game : newGames) {
game->moveToThread(this->thread());
game->setParent(nullptr);
}
QMetaObject::invokeMethod(
this,
[this, newGames]() {
for (Game *game : newGames) {
[this, games]() {
for (Game *game : games) {
m_gameModel->addGame(game);
}
setImportStatus(tr("RetroArch import complete: %1 new games found").arg(newGames.count()));
setImportStatus(tr("RetroArch import complete: %1 games found").arg(games.count()));
setImporting(false);
saveLibrary();
Q_EMIT importCompleted(newGames.count());
Q_EMIT importCompleted(games.count());
},
Qt::QueuedConnection);
});

View file

@ -279,74 +279,6 @@ void Config::setTheme(const QString &theme)
}
}
int Config::windowWidth() const
{
return m_windowWidth;
}
void Config::setWindowWidth(int width)
{
width = qBound(0, width, 10000);
if (m_windowWidth != width) {
m_windowWidth = width;
save();
Q_EMIT windowWidthChanged();
}
}
int Config::windowHeight() const
{
return m_windowHeight;
}
void Config::setWindowHeight(int height)
{
height = qBound(0, height, 10000);
if (m_windowHeight != height) {
m_windowHeight = height;
save();
Q_EMIT windowHeightChanged();
}
}
bool Config::windowMaximized() const
{
return m_windowMaximized;
}
void Config::setWindowMaximized(bool maximized)
{
if (m_windowMaximized != maximized) {
m_windowMaximized = maximized;
save();
Q_EMIT windowMaximizedChanged();
}
}
void Config::setWindowSize(int width, int height)
{
width = qBound(0, width, 10000);
height = qBound(0, height, 10000);
const bool widthChanged = m_windowWidth != width;
const bool heightChanged = m_windowHeight != height;
if (!widthChanged && !heightChanged) {
return;
}
m_windowWidth = width;
m_windowHeight = height;
save();
if (widthChanged) {
Q_EMIT windowWidthChanged();
}
if (heightChanged) {
Q_EMIT windowHeightChanged();
}
}
void Config::save()
{
const auto config = KSharedConfig::openConfig(QStringLiteral("alakarterc"));
@ -376,11 +308,6 @@ void Config::save()
behaviorGroup.writeEntry(QStringLiteral("exitAfterLaunch"), m_exitAfterLaunch);
behaviorGroup.writeEntry(QStringLiteral("coverLaunchesGame"), m_coverLaunchesGame);
KConfigGroup windowGroup(config, QStringLiteral("Window"));
windowGroup.writeEntry(QStringLiteral("width"), m_windowWidth);
windowGroup.writeEntry(QStringLiteral("height"), m_windowHeight);
windowGroup.writeEntry(QStringLiteral("maximized"), m_windowMaximized);
config->sync();
}
@ -412,11 +339,6 @@ void Config::load()
m_animatedCovers = behaviorGroup.readEntry(QStringLiteral("animatedCovers"), false);
m_exitAfterLaunch = behaviorGroup.readEntry(QStringLiteral("exitAfterLaunch"), false);
m_coverLaunchesGame = behaviorGroup.readEntry(QStringLiteral("coverLaunchesGame"), false);
const KConfigGroup windowGroup(config, QStringLiteral("Window"));
m_windowWidth = windowGroup.readEntry(QStringLiteral("width"), 0);
m_windowHeight = windowGroup.readEntry(QStringLiteral("height"), 0);
m_windowMaximized = windowGroup.readEntry(QStringLiteral("maximized"), false);
}
void Config::resetToDefaults()
@ -441,10 +363,6 @@ void Config::resetToDefaults()
m_animatedCovers = false;
m_theme.clear();
m_windowWidth = 0;
m_windowHeight = 0;
m_windowMaximized = false;
save();
Q_EMIT viewModeChanged();
@ -466,7 +384,4 @@ void Config::resetToDefaults()
Q_EMIT highQualityImagesChanged();
Q_EMIT animatedCoversChanged();
Q_EMIT themeChanged();
Q_EMIT windowWidthChanged();
Q_EMIT windowHeightChanged();
Q_EMIT windowMaximizedChanged();
}

View file

@ -30,9 +30,6 @@ class Config : public QObject
Q_PROPERTY(bool animatedCovers READ animatedCovers WRITE setAnimatedCovers NOTIFY animatedCoversChanged)
Q_PROPERTY(bool coverLaunchesGame READ coverLaunchesGame WRITE setCoverLaunchesGame NOTIFY coverLaunchesGameChanged)
Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)
Q_PROPERTY(int windowWidth READ windowWidth WRITE setWindowWidth NOTIFY windowWidthChanged)
Q_PROPERTY(int windowHeight READ windowHeight WRITE setWindowHeight NOTIFY windowHeightChanged)
Q_PROPERTY(bool windowMaximized READ windowMaximized WRITE setWindowMaximized NOTIFY windowMaximizedChanged)
public:
enum ViewMode {
@ -45,7 +42,7 @@ public:
enum UiMode {
Auto,
Desktop,
Couch
Handheld
};
Q_ENUM(UiMode)
@ -108,21 +105,10 @@ public:
QString theme() const;
void setTheme(const QString &theme);
int windowWidth() const;
void setWindowWidth(int width);
int windowHeight() const;
void setWindowHeight(int height);
bool windowMaximized() const;
void setWindowMaximized(bool maximized);
Q_INVOKABLE void save();
Q_INVOKABLE void load();
Q_INVOKABLE void resetToDefaults();
Q_INVOKABLE void setWindowSize(int width, int height);
Q_SIGNALS:
void viewModeChanged();
void uiModeChanged();
@ -143,9 +129,6 @@ Q_SIGNALS:
void animatedCoversChanged();
void coverLaunchesGameChanged();
void themeChanged();
void windowWidthChanged();
void windowHeightChanged();
void windowMaximizedChanged();
private:
ViewMode m_viewMode = GridView;
@ -167,8 +150,4 @@ private:
bool m_highQualityImages = true;
bool m_animatedCovers = false;
QString m_theme;
int m_windowWidth = 0;
int m_windowHeight = 0;
bool m_windowMaximized = false;
};

View file

@ -40,13 +40,7 @@ QStringList DesktopImporter::getDesktopFilePaths() const
// Add common system locations
appDirs << QStringLiteral("/usr/share/applications");
appDirs << QStringLiteral("/usr/local/share/applications");
const QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (!xdgDataHome.isEmpty()) {
appDirs << QDir(xdgDataHome).absoluteFilePath(QStringLiteral("applications"));
} else {
appDirs << expandPath(QStringLiteral("~/.local/share/applications"));
}
// Flatpak export directories
appDirs << expandPath(QStringLiteral("~/.local/share/flatpak/exports/share/applications"));

View file

@ -185,134 +185,6 @@ void Game::setLaunchPrefixPath(const QString &path)
}
}
QString Game::activeLaunchProfile() const
{
return m_activeLaunchProfile;
}
void Game::setActiveLaunchProfile(const QString &profileId)
{
const QString normalized = profileId.trimmed().isEmpty() ? QStringLiteral("default") : profileId.trimmed();
if (m_activeLaunchProfile != normalized) {
m_activeLaunchProfile = normalized;
Q_EMIT activeLaunchProfileChanged();
}
}
QVariantMap Game::launchProfileConfig(const QString &profileId) const
{
const QString id = profileId.trimmed();
if (id.isEmpty() || id == QLatin1String("default")) {
return {};
}
const QVariant value = m_launchProfiles.value(id);
if (!value.isValid()) {
return {};
}
return value.toMap();
}
void Game::setLaunchProfileConfig(const QString &profileId, const QVariantMap &config)
{
const QString id = profileId.trimmed();
if (id.isEmpty() || id == QLatin1String("default")) {
return;
}
QVariantMap cleaned;
const QString runner = config.value(QStringLiteral("runner")).toString().trimmed();
if (!runner.isEmpty()) {
cleaned.insert(QStringLiteral("runner"), runner);
}
const QString runnerPath = config.value(QStringLiteral("runnerPath")).toString().trimmed();
if (!runnerPath.isEmpty()) {
cleaned.insert(QStringLiteral("runnerPath"), runnerPath);
}
const QString prefixPath = config.value(QStringLiteral("prefixPath")).toString().trimmed();
if (!prefixPath.isEmpty()) {
cleaned.insert(QStringLiteral("prefixPath"), prefixPath);
}
const QVariantMap env = config.value(QStringLiteral("env")).toMap();
if (!env.isEmpty()) {
cleaned.insert(QStringLiteral("env"), env);
}
if (cleaned.isEmpty()) {
m_launchProfiles.remove(id);
} else {
m_launchProfiles.insert(id, cleaned);
}
}
QVariantMap Game::effectiveLaunchConfig() const
{
return effectiveLaunchConfigForProfile(m_activeLaunchProfile);
}
QVariantMap Game::effectiveLaunchConfigForProfile(const QString &profileId) const
{
QVariantMap result;
if (!m_launchRunner.isEmpty()) {
result.insert(QStringLiteral("runner"), m_launchRunner);
}
if (!m_launchRunnerPath.isEmpty()) {
result.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
}
if (!m_launchPrefixPath.isEmpty()) {
result.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
}
if (!m_launchEnv.isEmpty()) {
result.insert(QStringLiteral("env"), m_launchEnv);
}
const QString id = profileId.trimmed();
if (id.isEmpty() || id == QLatin1String("default")) {
return result;
}
const QVariantMap profileConfig = launchProfileConfig(id);
if (profileConfig.isEmpty()) {
return result;
}
const QString runner = profileConfig.value(QStringLiteral("runner")).toString().trimmed();
if (!runner.isEmpty()) {
result.insert(QStringLiteral("runner"), runner);
}
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
if (!runnerPath.isEmpty()) {
result.insert(QStringLiteral("runnerPath"), runnerPath);
}
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
if (!prefixPath.isEmpty()) {
result.insert(QStringLiteral("prefixPath"), prefixPath);
}
const QVariantMap profileEnv = profileConfig.value(QStringLiteral("env")).toMap();
if (!profileEnv.isEmpty()) {
QVariantMap mergedEnv;
const QVariantMap baseEnv = result.value(QStringLiteral("env")).toMap();
for (auto it = baseEnv.constBegin(); it != baseEnv.constEnd(); ++it) {
mergedEnv.insert(it.key(), it.value());
}
for (auto it = profileEnv.constBegin(); it != profileEnv.constEnd(); ++it) {
mergedEnv.insert(it.key(), it.value());
}
result.insert(QStringLiteral("env"), mergedEnv);
}
return result;
}
QString Game::platform() const
{
return m_platform;
@ -451,8 +323,7 @@ QJsonObject Game::toJson() const
obj[QStringLiteral("hidden")] = m_hidden;
obj[QStringLiteral("installed")] = m_installed;
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty()
|| !m_launchProfiles.isEmpty() || (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default"));
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty();
if (hasLaunchConfig) {
QJsonObject launchObj;
@ -474,57 +345,6 @@ QJsonObject Game::toJson() const
launchObj.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
}
if (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default")) {
launchObj.insert(QStringLiteral("activeProfile"), m_activeLaunchProfile);
}
if (!m_launchProfiles.isEmpty()) {
QJsonObject profilesObj;
for (auto it = m_launchProfiles.constBegin(); it != m_launchProfiles.constEnd(); ++it) {
const QString profileId = it.key();
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
continue;
}
const QVariantMap profileConfig = it.value().toMap();
if (profileConfig.isEmpty()) {
continue;
}
QJsonObject profileObj;
const QString runner = profileConfig.value(QStringLiteral("runner")).toString();
if (!runner.isEmpty()) {
profileObj.insert(QStringLiteral("runner"), runner);
}
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString();
if (!runnerPath.isEmpty()) {
profileObj.insert(QStringLiteral("runnerPath"), runnerPath);
}
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString();
if (!prefixPath.isEmpty()) {
profileObj.insert(QStringLiteral("prefixPath"), prefixPath);
}
const QVariantMap env = profileConfig.value(QStringLiteral("env")).toMap();
if (!env.isEmpty()) {
QJsonObject envObj;
for (auto envIt = env.constBegin(); envIt != env.constEnd(); ++envIt) {
envObj.insert(envIt.key(), QJsonValue::fromVariant(envIt.value()));
}
profileObj.insert(QStringLiteral("env"), envObj);
}
if (!profileObj.isEmpty()) {
profilesObj.insert(profileId, profileObj);
}
}
if (!profilesObj.isEmpty()) {
launchObj.insert(QStringLiteral("profiles"), profilesObj);
}
}
obj.insert(QStringLiteral("launch"), launchObj);
}
@ -573,7 +393,6 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
QString runner;
QString runnerPath;
QString prefixPath;
QString activeProfile;
const QJsonValue launchValue = json.value(QStringLiteral("launch"));
if (launchValue.isObject()) {
const QJsonObject launchObj = launchValue.toObject();
@ -585,51 +404,6 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
runner = launchObj.value(QStringLiteral("runner")).toString();
runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString();
prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString();
activeProfile = launchObj.value(QStringLiteral("activeProfile")).toString();
const QJsonValue profilesValue = launchObj.value(QStringLiteral("profiles"));
if (profilesValue.isObject()) {
const QJsonObject profilesObj = profilesValue.toObject();
for (auto it = profilesObj.constBegin(); it != profilesObj.constEnd(); ++it) {
if (!it.value().isObject()) {
continue;
}
const QString profileId = it.key();
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
continue;
}
const QJsonObject profileObj = it.value().toObject();
QVariantMap profileConfig;
const QString pRunner = profileObj.value(QStringLiteral("runner")).toString();
if (!pRunner.isEmpty()) {
profileConfig.insert(QStringLiteral("runner"), pRunner);
}
const QString pRunnerPath = profileObj.value(QStringLiteral("runnerPath")).toString();
if (!pRunnerPath.isEmpty()) {
profileConfig.insert(QStringLiteral("runnerPath"), pRunnerPath);
}
const QString pPrefixPath = profileObj.value(QStringLiteral("prefixPath")).toString();
if (!pPrefixPath.isEmpty()) {
profileConfig.insert(QStringLiteral("prefixPath"), pPrefixPath);
}
const QJsonValue pEnvValue = profileObj.value(QStringLiteral("env"));
if (pEnvValue.isObject()) {
const QVariantMap pEnv = pEnvValue.toObject().toVariantMap();
if (!pEnv.isEmpty()) {
profileConfig.insert(QStringLiteral("env"), pEnv);
}
}
if (!profileConfig.isEmpty()) {
game->setLaunchProfileConfig(profileId, profileConfig);
}
}
}
}
const QJsonValue legacyEnvValue = json.value(QStringLiteral("launchEnv"));
@ -661,10 +435,6 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
game->setLaunchPrefixPath(prefixPath);
}
if (!activeProfile.isEmpty()) {
game->setActiveLaunchProfile(activeProfile);
}
return game;
}

View file

@ -30,7 +30,6 @@ class Game : public QObject
Q_PROPERTY(QString launchRunner READ launchRunner WRITE setLaunchRunner NOTIFY launchRunnerChanged)
Q_PROPERTY(QString launchRunnerPath READ launchRunnerPath WRITE setLaunchRunnerPath NOTIFY launchRunnerPathChanged)
Q_PROPERTY(QString launchPrefixPath READ launchPrefixPath WRITE setLaunchPrefixPath NOTIFY launchPrefixPathChanged)
Q_PROPERTY(QString activeLaunchProfile READ activeLaunchProfile WRITE setActiveLaunchProfile NOTIFY activeLaunchProfileChanged)
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
Q_PROPERTY(QString platformId READ platformId WRITE setPlatformId NOTIFY platformIdChanged)
Q_PROPERTY(QDateTime dateAdded READ dateAdded WRITE setDateAdded NOTIFY dateAddedChanged)
@ -83,14 +82,6 @@ public:
QString launchPrefixPath() const;
void setLaunchPrefixPath(const QString &path);
QString activeLaunchProfile() const;
void setActiveLaunchProfile(const QString &profileId);
Q_INVOKABLE QVariantMap launchProfileConfig(const QString &profileId) const;
Q_INVOKABLE void setLaunchProfileConfig(const QString &profileId, const QVariantMap &config);
QVariantMap effectiveLaunchConfigForProfile(const QString &profileId) const;
QVariantMap effectiveLaunchConfig() const;
QString platform() const;
void setPlatform(const QString &platform);
@ -138,7 +129,6 @@ Q_SIGNALS:
void launchRunnerChanged();
void launchRunnerPathChanged();
void launchPrefixPathChanged();
void activeLaunchProfileChanged();
void platformChanged();
void platformIdChanged();
void dateAddedChanged();
@ -163,8 +153,6 @@ private:
QString m_launchRunner;
QString m_launchRunnerPath;
QString m_launchPrefixPath;
QString m_activeLaunchProfile = QStringLiteral("default");
QVariantMap m_launchProfiles;
QString m_platform;
QString m_platformId;
QDateTime m_dateAdded;

View file

@ -3,8 +3,6 @@
#include "gamelauncher.h"
#include "app.h"
#include "gamepadmanager.h"
#include "inputmanager.h"
#include <QCoreApplication>
#include <QDateTime>
@ -110,33 +108,6 @@ static QString discoverDefaultProtonExecutable()
return cached;
}
static QString profileIdForCurrentUiMode()
{
const Config *config = App::instance() ? App::instance()->config() : nullptr;
if (!config) {
return QStringLiteral("default");
}
if (config->uiMode() == Config::Couch) {
return QStringLiteral("couch");
}
if (config->uiMode() == Config::Auto) {
GamepadManager *pad = GamepadManager::instance();
InputManager *input = InputManager::instance();
if (pad && pad->connected()) {
const bool activeGamepad = input && input->activeInput() == InputManager::Gamepad;
const bool noKeyboardMouse = input && !input->hasSeenKeyboardMouse();
if (activeGamepad || noKeyboardMouse) {
return QStringLiteral("couch");
}
}
}
return QStringLiteral("default");
}
GameLauncher::GameLauncher(QObject *parent)
: QObject(parent)
{
@ -161,161 +132,6 @@ bool GameLauncher::hasRunningGames() const
return !m_runningGames.isEmpty();
}
QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
{
QVariantMap info;
info.insert(QStringLiteral("ok"), false);
if (!game) {
info.insert(QStringLiteral("error"), tr("No game selected"));
return info;
}
info.insert(QStringLiteral("gameId"), game->id());
info.insert(QStringLiteral("gameName"), game->name());
info.insert(QStringLiteral("isRunning"), m_runningGames.contains(game->id()));
const QString command = game->launchCommand();
info.insert(QStringLiteral("launchCommand"), command);
if (command.isEmpty()) {
info.insert(QStringLiteral("error"), tr("No launch command configured"));
return info;
}
const QString profileId = profileIdForCurrentUiMode();
const QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfigForProfile(profileId);
const QString runner = effectiveLaunchConfig.value(QStringLiteral("runner")).toString().trimmed();
const QString runnerPath = effectiveLaunchConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
const QString prefixPath = effectiveLaunchConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
const QVariantMap launchEnv = effectiveLaunchConfig.value(QStringLiteral("env")).toMap();
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
info.insert(QStringLiteral("runner"), runner);
info.insert(QStringLiteral("runnerPath"), runnerPath);
info.insert(QStringLiteral("prefixPath"), prefixPath);
info.insert(QStringLiteral("workingDirectory"), game->workingDirectory());
info.insert(QStringLiteral("hasLaunchOverrides"), hasLaunchOverrides);
QVariantMap envOverrides;
for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) {
const QString key = it.key();
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
continue;
}
envOverrides.insert(key, it.value().toString());
}
info.insert(QStringLiteral("envOverrides"), envOverrides);
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) {
info.insert(QStringLiteral("ok"), true);
info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
info.insert(QStringLiteral("url"), command);
info.insert(QStringLiteral("usesDesktopServices"), true);
return info;
}
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) {
info.insert(QStringLiteral("ok"), true);
info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
info.insert(QStringLiteral("url"), command.mid(7));
info.insert(QStringLiteral("usesDesktopServices"), true);
return info;
}
QStringList parts;
if (command.startsWith(QLatin1String("steam://"))) {
parts = {QStringLiteral("xdg-open"), command};
} else if (command.startsWith(QLatin1String("lutris "))) {
parts = {QStringLiteral("xdg-open"), command.mid(7)};
} else {
parts = QProcess::splitCommand(command);
}
if (parts.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Invalid launch command"));
return info;
}
const QString program = parts.takeFirst();
info.insert(QStringLiteral("program"), program);
info.insert(QStringLiteral("args"), parts);
QString finalProgram = program;
QStringList finalArgs = parts;
QVariantMap effectiveEnv = envOverrides;
if ((runner == QLatin1String("wine") || runner == QLatin1String("proton")) && (program == QLatin1String("xdg-open"))) {
info.insert(QStringLiteral("error"), tr("This runner cannot be used with URL-based launch commands"));
return info;
}
if (runner == QLatin1String("custom")) {
if (runnerPath.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Custom runner is enabled but no runner path is configured"));
return info;
}
finalProgram = runnerPath;
finalArgs = {program};
finalArgs.append(parts);
} else if (runner == QLatin1String("wine")) {
QString wineExe = runnerPath;
if (wineExe.isEmpty()) {
wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
}
if (wineExe.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Wine runner is enabled but Wine was not found"));
return info;
}
QString winePrefix = prefixPath;
if (winePrefix.isEmpty()) {
winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
}
effectiveEnv.insert(QStringLiteral("WINEPREFIX"), winePrefix);
info.insert(QStringLiteral("resolvedPrefixPath"), winePrefix);
finalProgram = wineExe;
finalArgs = {program};
finalArgs.append(parts);
} else if (runner == QLatin1String("proton")) {
QString protonExe = runnerPath;
if (protonExe.isEmpty()) {
protonExe = discoverDefaultProtonExecutable();
}
if (protonExe.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Proton runner is enabled but no Proton installation was found"));
return info;
}
const QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe);
info.insert(QStringLiteral("resolvedSteamInstallPath"), steamInstallPath);
QString compatDataPath = prefixPath;
if (compatDataPath.isEmpty()) {
compatDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
}
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), compatDataPath);
info.insert(QStringLiteral("resolvedPrefixPath"), compatDataPath);
if (!steamInstallPath.isEmpty()) {
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath);
}
finalProgram = protonExe;
finalArgs = {QStringLiteral("run"), program};
finalArgs.append(parts);
}
info.insert(QStringLiteral("finalProgram"), finalProgram);
info.insert(QStringLiteral("finalArgs"), finalArgs);
info.insert(QStringLiteral("effectiveEnv"), effectiveEnv);
info.insert(QStringLiteral("ok"), true);
return info;
}
void GameLauncher::launchGame(Game *game)
{
if (!game) {
@ -328,12 +144,10 @@ void GameLauncher::launchGame(Game *game)
return;
}
const QString profileId = profileIdForCurrentUiMode();
const QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfigForProfile(profileId);
const QString runner = effectiveLaunchConfig.value(QStringLiteral("runner")).toString().trimmed();
const QString runnerPath = effectiveLaunchConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
const QString prefixPath = effectiveLaunchConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
const QVariantMap launchEnv = effectiveLaunchConfig.value(QStringLiteral("env")).toMap();
const QString runner = game->launchRunner().trimmed();
const QString runnerPath = game->launchRunnerPath().trimmed();
const QString prefixPath = game->launchPrefixPath().trimmed();
const QVariantMap launchEnv = game->launchEnv();
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
// Check if already running

View file

@ -7,7 +7,6 @@
#include <QObject>
#include <QProcess>
#include <QQmlEngine>
#include <QVariantMap>
#include "game.h"
@ -25,7 +24,6 @@ public:
bool hasRunningGames() const;
Q_INVOKABLE void launchGame(Game *game);
Q_INVOKABLE QVariantMap resolveLaunchInfo(Game *game) const;
Q_INVOKABLE void stopGame(Game *game);
Q_INVOKABLE bool isGameRunning(Game *game) const;

View file

@ -1,104 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "inputmanager.h"
#include "gamepadmanager.h"
#include <QCoreApplication>
#include <QEvent>
#include <QGuiApplication>
InputManager *InputManager::s_instance = nullptr;
InputManager::InputManager(QObject *parent)
: QObject(parent)
{
if (QCoreApplication::instance()) {
QCoreApplication::instance()->installEventFilter(this);
}
auto *pad = GamepadManager::instance();
connect(pad, &GamepadManager::activeChanged, this, [this, pad]() {
if (pad->active()) {
if (!m_hasSeenGamepad) {
m_hasSeenGamepad = true;
Q_EMIT inputHistoryChanged();
}
setActiveInput(Gamepad);
}
});
connect(pad, &GamepadManager::connectedChanged, this, [this, pad]() {
if (!pad->connected() && m_activeInput == Gamepad) {
setActiveInput(KeyboardMouse);
}
});
}
InputManager *InputManager::instance()
{
if (!s_instance) {
s_instance = new InputManager();
}
return s_instance;
}
InputManager *InputManager::create(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
Q_UNUSED(scriptEngine)
return instance();
}
InputManager::ActiveInput InputManager::activeInput() const
{
return m_activeInput;
}
bool InputManager::hasSeenKeyboardMouse() const
{
return m_hasSeenKeyboardMouse;
}
bool InputManager::hasSeenGamepad() const
{
return m_hasSeenGamepad;
}
void InputManager::setActiveInput(ActiveInput input)
{
if (m_activeInput == input) {
return;
}
m_activeInput = input;
Q_EMIT activeInputChanged();
}
bool InputManager::eventFilter(QObject *watched, QEvent *event)
{
Q_UNUSED(watched)
if (!event) {
return false;
}
switch (event->type()) {
case QEvent::KeyPress:
case QEvent::KeyRelease:
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::Wheel:
if (!m_hasSeenKeyboardMouse) {
m_hasSeenKeyboardMouse = true;
Q_EMIT inputHistoryChanged();
}
setActiveInput(KeyboardMouse);
break;
default:
break;
}
return false;
}

View file

@ -1,54 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QObject>
#include <QQmlEngine>
class QEvent;
class QJSEngine;
class InputManager : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(ActiveInput activeInput READ activeInput NOTIFY activeInputChanged)
Q_PROPERTY(bool hasSeenKeyboardMouse READ hasSeenKeyboardMouse NOTIFY inputHistoryChanged)
Q_PROPERTY(bool hasSeenGamepad READ hasSeenGamepad NOTIFY inputHistoryChanged)
public:
enum ActiveInput {
KeyboardMouse = 0,
Gamepad = 1,
};
Q_ENUM(ActiveInput)
static InputManager *instance();
static InputManager *create(QQmlEngine *engine, QJSEngine *scriptEngine);
ActiveInput activeInput() const;
bool hasSeenKeyboardMouse() const;
bool hasSeenGamepad() const;
Q_SIGNALS:
void activeInputChanged();
void inputHistoryChanged();
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
explicit InputManager(QObject *parent = nullptr);
static InputManager *s_instance;
ActiveInput m_activeInput = KeyboardMouse;
bool m_hasSeenKeyboardMouse = false;
bool m_hasSeenGamepad = false;
void setActiveInput(ActiveInput input);
};

View file

@ -1,15 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QByteArray>
#include <QCommandLineParser>
#include <QGuiApplication>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <QTextStream>
#include <QTimer>
#include <KAboutData>
#include <KCrash>
@ -18,11 +15,9 @@
#include <KLocalizedString>
#include "alakarte-version.h"
#include "app.h"
int main(int argc, char *argv[])
{
qputenv("QT_DESKTOP_FILE_NAME", QByteArray("org.kde.alakarte"));
QGuiApplication app(argc, argv);
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
@ -50,54 +45,15 @@ int main(int argc, char *argv[])
QCommandLineParser parser;
aboutData.setupCommandLine(&parser);
QCommandLineOption importAllAndExitOption(QStringLiteral("import-all-and-exit"), i18n("Import all enabled sources and exit"));
QCommandLineOption importDesktopAndExitOption(QStringLiteral("import-desktop-and-exit"), i18n("Import desktop entries and exit"));
QCommandLineOption startupAndExitOption(QStringLiteral("startup-and-exit"), i18n("Start without UI and exit after startup auto-import (if enabled)"));
parser.addOption(importAllAndExitOption);
parser.addOption(importDesktopAndExitOption);
parser.addOption(startupAndExitOption);
parser.process(app);
aboutData.processCommandLine(&parser);
KDBusService service(KDBusService::Unique);
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
}
if (parser.isSet(importAllAndExitOption) || parser.isSet(importDesktopAndExitOption) || parser.isSet(startupAndExitOption)) {
App *alakarteApp = App::instance();
QObject::connect(alakarteApp, &App::importCompleted, &app, [&app](int count) {
QTextStream(stdout) << count << Qt::endl;
app.exit(0);
});
QObject::connect(alakarteApp, &App::importError, &app, [&app](const QString &error) {
QTextStream(stderr) << error << Qt::endl;
app.exit(1);
});
if (parser.isSet(importAllAndExitOption)) {
alakarteApp->importAllGames();
} else if (parser.isSet(importDesktopAndExitOption)) {
alakarteApp->importFromDesktop();
} else {
if (!alakarteApp->config() || !alakarteApp->config()->autoImportOnStartup()) {
QTextStream(stdout) << 0 << Qt::endl;
return 0;
}
QTimer::singleShot(60000, &app, [&app]() {
QTextStream(stderr) << QStringLiteral("Timed out waiting for auto-import") << Qt::endl;
app.exit(2);
});
}
return app.exec();
}
KDBusService service(KDBusService::Unique);
QQmlApplicationEngine engine;
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));

View file

@ -1,126 +0,0 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.alakarte
Kirigami.OverlaySheet {
id: diagnosticsSheet
property var game
property var launchInfo: ({})
title: i18n("Diagnostics")
parent: applicationWindow().overlay
closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutside
implicitWidth: {
let w = applicationWindow()
if (!w) return Kirigami.Units.gridUnit * 30
if (w.isMobile) return w.width
if (w.isNarrowScreen) return w.width - Kirigami.Units.largeSpacing * 2
return Math.min(w.width - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 34)
}
implicitHeight: {
let w = applicationWindow()
if (!w) return Kirigami.Units.gridUnit * 30
if (w.isMobile) return w.height
return Math.min(w.height - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 42)
}
onOpened: {
if (game) {
launchInfo = App.launcher.resolveLaunchInfo(game)
} else {
launchInfo = ({ "ok": false, "error": i18n("No game selected") })
}
closeButton.forceActiveFocus()
}
header: Kirigami.ShadowedRectangle {
implicitHeight: headerRow.implicitHeight + Kirigami.Units.largeSpacing * 2
radius: Kirigami.Units.mediumSpacing
color: Kirigami.Theme.backgroundColor
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.mediumSpacing
Kirigami.Heading {
text: diagnosticsSheet.title
level: 2
Layout.fillWidth: true
elide: Text.ElideRight
}
QQC2.ToolButton {
id: closeButton
text: i18n("Close")
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: diagnosticsSheet.close()
}
}
}
contentItem: QQC2.ScrollView {
id: scroll
clip: true
ColumnLayout {
width: scroll.availableWidth
spacing: Kirigami.Units.largeSpacing
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: launchInfo && launchInfo.ok === false
type: Kirigami.MessageType.Error
text: launchInfo && launchInfo.error ? launchInfo.error : i18n("Unknown error")
}
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.Label {
Kirigami.FormData.label: i18n("Launch command:")
text: launchInfo && launchInfo.launchCommand ? launchInfo.launchCommand : ""
wrapMode: Text.WrapAnywhere
}
QQC2.Label {
Kirigami.FormData.label: i18n("Runner:")
text: launchInfo && launchInfo.runner ? launchInfo.runner : i18n("None")
wrapMode: Text.WrapAnywhere
}
QQC2.Label {
Kirigami.FormData.label: i18n("Program:")
text: launchInfo && launchInfo.finalProgram ? launchInfo.finalProgram : ""
wrapMode: Text.WrapAnywhere
}
QQC2.Label {
Kirigami.FormData.label: i18n("Args:")
text: launchInfo && launchInfo.finalArgs ? launchInfo.finalArgs.join(" ") : ""
wrapMode: Text.WrapAnywhere
}
QQC2.Label {
Kirigami.FormData.label: i18n("Working directory:")
text: launchInfo && launchInfo.workingDirectory ? launchInfo.workingDirectory : i18n("Not specified")
wrapMode: Text.WrapAnywhere
}
QQC2.Label {
Kirigami.FormData.label: i18n("Prefix:")
text: launchInfo && launchInfo.resolvedPrefixPath ? launchInfo.resolvedPrefixPath : (launchInfo && launchInfo.prefixPath ? launchInfo.prefixPath : "")
wrapMode: Text.WrapAnywhere
}
}
}
}
}

View file

@ -12,18 +12,7 @@ import org.kde.alakarte
Kirigami.OverlaySheet {
id: detailsSheet
property var game: null
property int lastNonCouchUiMode: Config.Auto
readonly property bool effectiveCouchMode: {
if (App.config.uiMode === Config.Couch) return true
if (App.config.uiMode !== Config.Auto) return false
if (GamepadManager.connected) {
if (InputManager.activeInput === InputManager.Gamepad) return true
if (!InputManager.hasSeenKeyboardMouse) return true
}
return false
}
property var game
readonly property var screenshotsModel: game ? App.mediaManager.screenshotsModel(game) : null
@ -59,44 +48,16 @@ Kirigami.OverlaySheet {
signal launchRequested()
signal editRequested()
signal diagnosticsRequested()
signal removeRequested()
title: game ? game.name : ""
onOpened: playButton.forceActiveFocus()
Shortcut {
enabled: detailsSheet.opened
sequence: "E"
onActivated: {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
detailsSheet.editRequested()
}
}
Shortcut {
enabled: detailsSheet.opened
sequence: "F"
onActivated: {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (game) game.favorite = !game.favorite
}
}
function isDescendant(item, ancestor) {
let p = item
while (p) {
if (p === ancestor) return true
if (ancestor.contentItem && p === ancestor.contentItem) return true
if (ancestor.header && p === ancestor.header) return true
if (p.visualParent !== undefined && p.visualParent !== null) {
if (detailsSheet.isDescendant(p.visualParent, ancestor)) return true
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
if (detailsSheet.isDescendant(p.popup.visualParent, ancestor)) return true
}
p = p.parent
}
return false
@ -120,8 +81,6 @@ Kirigami.OverlaySheet {
Connections {
target: GamepadManager
function onNavigateLeft() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!detailsSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -129,8 +88,6 @@ Kirigami.OverlaySheet {
detailsSheet.focusNextInChain(false)
}
function onNavigateRight() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!detailsSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -138,8 +95,6 @@ Kirigami.OverlaySheet {
detailsSheet.focusNextInChain(true)
}
function onNavigateUp() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!detailsSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -147,8 +102,6 @@ Kirigami.OverlaySheet {
detailsSheet.focusNextInChain(false)
}
function onNavigateDown() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!detailsSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -156,8 +109,6 @@ Kirigami.OverlaySheet {
detailsSheet.focusNextInChain(true)
}
function onSelectPressed() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!detailsSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -429,32 +380,6 @@ Kirigami.OverlaySheet {
onClicked: detailsSheet.editRequested()
}
QQC2.Button {
icon.name: "view-fullscreen"
text: detailsSheet.effectiveCouchMode ? i18n("Couch mode: On") : i18n("Couch mode: Off")
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
onClicked: {
if (detailsSheet.effectiveCouchMode) {
if (detailsSheet.lastNonCouchUiMode === Config.Auto) {
App.config.uiMode = Config.Desktop
} else {
App.config.uiMode = detailsSheet.lastNonCouchUiMode
}
} else {
detailsSheet.lastNonCouchUiMode = App.config.uiMode
App.config.uiMode = Config.Couch
}
}
}
QQC2.Button {
icon.name: "dialog-information"
text: i18n("Diagnostics")
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
enabled: !!game
onClicked: detailsSheet.diagnosticsRequested()
}
QQC2.Button {
icon.name: "edit-delete"
text: i18n("Remove")

View file

@ -14,7 +14,6 @@ Kirigami.Dialog {
property var game: null
property bool isEditing: game !== null
property string editProfileId: "default"
ListModel {
id: envModel
@ -50,19 +49,10 @@ Kirigami.Dialog {
game.developer = developerField.text.trim()
game.launchCommand = executableField.text.trim()
game.workingDirectory = workingDirField.text.trim()
if (dialog.editProfileId === "couch") {
game.setLaunchProfileConfig("couch", {
"runner": runnerValue,
"runnerPath": runnerPathValue,
"prefixPath": prefixPathValue,
"env": dialog.envModelToMap()
})
} else {
game.launchEnv = dialog.envModelToMap()
game.launchRunner = runnerValue
game.launchRunnerPath = runnerPathValue
game.launchPrefixPath = prefixPathValue
}
if (selectedCoverPath !== "") {
App.setCoverFromFile(game, selectedCoverPath)
}
@ -96,26 +86,10 @@ Kirigami.Dialog {
property string selectedCoverPath: ""
readonly property bool anyMenuOpen: (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) || (profileCombo && profileCombo.popup && profileCombo.popup.visible)
function closeCurrentMenu() {
if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) {
runnerCombo.popup.close()
}
if (profileCombo && profileCombo.popup && profileCombo.popup.visible) {
profileCombo.popup.close()
}
}
function isDescendant(item, ancestor) {
let p = item
while (p) {
if (p === ancestor || (ancestor.contentItem && p === ancestor.contentItem)) return true
if (p.visualParent !== undefined && p.visualParent !== null) {
if (dialog.isDescendant(p.visualParent, ancestor)) return true
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
if (dialog.isDescendant(p.popup.visualParent, ancestor)) return true
}
p = p.parent
}
return false
@ -144,7 +118,7 @@ Kirigami.Dialog {
item.toggle()
return
}
if (item.checkable !== undefined && item.checkable && item.checked !== undefined) {
if (item.hasOwnProperty("checked")) {
item.checked = !item.checked
return
}
@ -194,42 +168,19 @@ Kirigami.Dialog {
return 0
}
function profileIdFromIndex(idx) {
if (idx === 1) return "couch"
return "default"
}
function profileIndexFromId(profileId) {
if ((profileId || "").trim() === "couch") return 1
return 0
}
function loadProfileFields() {
function loadFields() {
selectedCoverPath = ""
envModel.clear()
if (isEditing && game) {
nameField.text = game.name || ""
developerField.text = game.developer || ""
executableField.text = game.launchCommand || ""
workingDirField.text = game.workingDirectory || ""
if (!isEditing || !game) {
runnerCombo.currentIndex = 0
runnerPathField.text = ""
prefixPathField.text = ""
return
}
if (dialog.editProfileId === "couch") {
let cfg = game.launchProfileConfig("couch") || ({})
runnerCombo.currentIndex = dialog.runnerToIndex(cfg.runner)
runnerPathField.text = cfg.runnerPath || ""
prefixPathField.text = cfg.prefixPath || ""
let env = cfg.env || ({})
let keys = Object.keys(env)
keys.sort()
for (let i = 0; i < keys.length; i++) {
let k = keys[i]
envModel.append({ key: k, value: String(env[k]) })
}
} else {
runnerCombo.currentIndex = dialog.runnerToIndex(game.launchRunner)
runnerPathField.text = game.launchRunnerPath || ""
prefixPathField.text = game.launchPrefixPath || ""
let env = game.launchEnv || ({})
let keys = Object.keys(env)
keys.sort()
@ -237,42 +188,21 @@ Kirigami.Dialog {
let k = keys[i]
envModel.append({ key: k, value: String(env[k]) })
}
}
}
function loadFields() {
selectedCoverPath = ""
if (isEditing && game) {
nameField.text = game.name || ""
developerField.text = game.developer || ""
executableField.text = game.launchCommand || ""
workingDirField.text = game.workingDirectory || ""
dialog.editProfileId = (App.config.uiMode === Config.Couch
|| (App.config.uiMode === Config.Auto
&& GamepadManager.connected
&& (InputManager.activeInput === InputManager.Gamepad || !InputManager.hasSeenKeyboardMouse)))
? "couch"
: "default"
profileCombo.currentIndex = dialog.profileIndexFromId(dialog.editProfileId)
dialog.loadProfileFields()
} else {
nameField.text = ""
developerField.text = ""
executableField.text = ""
workingDirField.text = ""
dialog.editProfileId = "default"
profileCombo.currentIndex = 0
dialog.loadProfileFields()
runnerCombo.currentIndex = 0
runnerPathField.text = ""
prefixPathField.text = ""
}
}
Connections {
target: GamepadManager
function onNavigateUp() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -280,8 +210,6 @@ Kirigami.Dialog {
dialog.focusNextInChain(false)
}
function onNavigateDown() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -289,8 +217,6 @@ Kirigami.Dialog {
dialog.focusNextInChain(true)
}
function onNavigateLeft() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -298,8 +224,6 @@ Kirigami.Dialog {
dialog.focusNextInChain(false)
}
function onNavigateRight() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -307,8 +231,6 @@ Kirigami.Dialog {
dialog.focusNextInChain(true)
}
function onSelectPressed() {
let app = applicationWindow()
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
@ -380,35 +302,6 @@ Kirigami.Dialog {
title: i18n("Compatibility")
}
Item {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
visible: isEditing
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Profile")
Layout.alignment: Qt.AlignVCenter
}
QQC2.ComboBox {
id: profileCombo
Layout.fillWidth: true
model: [i18n("Default"), i18n("Couch")]
onCurrentIndexChanged: if (dialog.isEditing) {
dialog.editProfileId = dialog.profileIdFromIndex(currentIndex)
dialog.loadProfileFields()
}
}
}
}
FormCard.FormDelegateSeparator { visible: isEditing }
Item {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing

View file

@ -25,12 +25,6 @@ FocusScope {
readonly property int gameCount: proxyModel.count
property url focusedCoverUrl: ""
readonly property bool anyMenuOpen: searchHeader.anyMenuOpen
function closeCurrentMenu() {
searchHeader.closeCurrentMenu()
}
function focusSearch() {
searchField.forceActiveFocus()
}
@ -41,14 +35,6 @@ FocusScope {
}
function restoreFocus() {
let w = applicationWindow()
if (w && w.hasOwnProperty("pendingSidebarOpen") && w.pendingSidebarOpen) {
w.pendingSidebarOpen = false
if (w.globalDrawer && typeof w.globalDrawer.open === "function") {
w.globalDrawer.open()
return
}
}
if (libraryRoot.searchActive) {
libraryRoot.focusSearch()
} else {
@ -190,16 +176,6 @@ FocusScope {
}
Keys.onDownPressed: gameGrid.forceActiveFocus()
Connections {
target: GamepadManager
function onNavigateDown() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (!searchField.activeFocus) return
gameGrid.forceActiveFocus()
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -20,96 +20,6 @@ ColumnLayout {
property var pendingDisableImportApply: null
property string pendingDisableImportName: ""
readonly property bool anyConfirmOpen: disableImportConfirmDialog.opened
|| fetchAllCoversConfirmDialog.opened
|| removeMissingConfirmDialog.opened
|| clearConfirmDialog.opened
|| resetConfirmDialog.opened
readonly property bool anyMenuOpen: uiModeMenu.visible
function currentConfirmDialog() {
if (disableImportConfirmDialog.opened) return disableImportConfirmDialog
if (fetchAllCoversConfirmDialog.opened) return fetchAllCoversConfirmDialog
if (removeMissingConfirmDialog.opened) return removeMissingConfirmDialog
if (clearConfirmDialog.opened) return clearConfirmDialog
if (resetConfirmDialog.opened) return resetConfirmDialog
return null
}
function closeCurrentConfirmDialog() {
let d = currentConfirmDialog()
if (!d) return
if (typeof d.reject === "function") {
d.reject()
} else {
d.close()
}
}
function closeCurrentMenu() {
if (uiModeMenu.visible) {
uiModeMenu.close()
}
}
function isDescendant(item, ancestor) {
let p = item
while (p) {
if (p === ancestor) return true
if (ancestor.contentItem && p === ancestor.contentItem) return true
if (p.visualParent !== undefined && p.visualParent !== null) {
if (settingsPage.isDescendant(p.visualParent, ancestor)) return true
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
if (settingsPage.isDescendant(p.popup.visualParent, ancestor)) return true
}
p = p.parent
}
return false
}
function focusNextInMenu(forward) {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let next = w.activeFocusItem
for (let i = 0; i < 50; i++) {
next = next.nextItemInFocusChain(forward)
if (!next) return
if (settingsPage.isDescendant(next, uiModeMenu)) {
next.forceActiveFocus()
return
}
}
}
function activateFocusedInMenu() {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let item = w.activeFocusItem
if (typeof item.triggered === "function") {
item.triggered()
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
function focusNoButton(dialog) {
Qt.callLater(function() {
if (!dialog) return
if (typeof dialog.standardButton === "function") {
let noButton = dialog.standardButton(Kirigami.Dialog.No)
if (noButton) {
noButton.forceActiveFocus()
return
}
}
dialog.forceActiveFocus()
})
}
function requestDisableImport(sourceName, applyFn) {
pendingDisableImportName = sourceName
pendingDisableImportApply = applyFn
@ -138,7 +48,7 @@ ColumnLayout {
text: i18n("UI mode")
description: {
if (App.config.uiMode === Config.Desktop) return i18n("Desktop")
if (App.config.uiMode === Config.Couch) return i18n("Couch")
if (App.config.uiMode === Config.Handheld) return i18n("Handheld")
return i18n("Automatic")
}
icon.name: "view-fullscreen"
@ -146,12 +56,8 @@ ColumnLayout {
QQC2.Menu {
id: uiModeMenu
focus: true
onOpened: Qt.callLater(function() { uiModeAuto.forceActiveFocus() })
QQC2.MenuItem {
id: uiModeAuto
text: i18n("Automatic")
checkable: true
checked: App.config.uiMode === Config.Auto
@ -159,7 +65,6 @@ ColumnLayout {
}
QQC2.MenuItem {
id: uiModeDesktop
text: i18n("Desktop")
checkable: true
checked: App.config.uiMode === Config.Desktop
@ -167,55 +72,15 @@ ColumnLayout {
}
QQC2.MenuItem {
id: uiModeCouch
text: i18n("Couch")
text: i18n("Handheld")
checkable: true
checked: App.config.uiMode === Config.Couch
onTriggered: App.config.uiMode = Config.Couch
checked: App.config.uiMode === Config.Handheld
onTriggered: App.config.uiMode = Config.Handheld
}
}
}
}
Connections {
target: GamepadManager
function onNavigateUp() {
if (!uiModeMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
settingsPage.focusNextInMenu(false)
}
function onNavigateDown() {
if (!uiModeMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
settingsPage.focusNextInMenu(true)
}
function onNavigateLeft() {
if (!uiModeMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
settingsPage.focusNextInMenu(false)
}
function onNavigateRight() {
if (!uiModeMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
settingsPage.focusNextInMenu(true)
}
function onSelectPressed() {
if (!uiModeMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
settingsPage.activateFocusedInMenu()
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
@ -643,15 +508,15 @@ FormCard.FormHeader {
text: i18n("Import All Games")
description: i18n("Scan all enabled sources")
icon.name: "document-import"
enabled: !App.importing && App.gameModel.count >= 0 && (App.config.importSteam
|| App.config.importLutris
|| App.config.importHeroic
|| App.config.importDesktop
|| App.config.importBottles
|| App.config.importFlatpak
|| App.config.importItch
|| App.config.importLegendary
|| App.config.importRetroArch)
enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam"))
|| (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris"))
|| (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic"))
|| (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop"))
|| (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles"))
|| (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak"))
|| (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io"))
|| (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary"))
|| (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch")))
onClicked: App.importAllGames()
}
@ -688,7 +553,6 @@ FormCard.FormHeader {
title: i18n("Disable Import Source")
subtitle: i18n("Disabling %1 will remove all games imported from that source. Are you sure?", settingsPage.pendingDisableImportName)
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(disableImportConfirmDialog)
onAccepted: {
if (settingsPage.pendingDisableImportApply) {
settingsPage.pendingDisableImportApply()
@ -709,7 +573,6 @@ FormCard.FormHeader {
? i18n("This will download cover art for all games and may replace existing covers. Continue?")
: i18n("This will download cover art for games that are missing covers. Continue?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(fetchAllCoversConfirmDialog)
onAccepted: App.steamGridDB.fetchAllCovers()
}
@ -718,7 +581,6 @@ FormCard.FormHeader {
title: i18n("Remove Missing Games")
subtitle: i18n("This will remove games whose executables cannot be found. This cannot be undone. Continue?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(removeMissingConfirmDialog)
onAccepted: App.removeMissingGames()
}
@ -727,7 +589,6 @@ FormCard.FormHeader {
title: i18n("Clear Library")
subtitle: i18n("Are you sure you want to remove all games?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(clearConfirmDialog)
onAccepted: App.clearLibrary()
}
@ -736,7 +597,6 @@ FormCard.FormHeader {
title: i18n("Reset Application")
subtitle: i18n("This will remove all games and reset all settings to defaults. Are you sure?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(resetConfirmDialog)
onAccepted: {
App.clearLibrary()
App.config.resetToDefaults()

View file

@ -16,8 +16,6 @@ ColumnLayout {
property string currentSourceName: i18n("All Games")
property bool suppressAutoClose: false
readonly property bool hasSidebarFocus: sourceList.activeFocus || importAction.activeFocus || settingsAction.activeFocus || aboutAction.activeFocus
readonly property int adaptiveFocusRingWidth: 1
signal sourceSelected(string source)
@ -47,17 +45,6 @@ ColumnLayout {
sourceList.positionViewAtIndex(i, ListView.Contain)
}
function applySourceById(sourceId) {
for (let i = 0; i < sourceModel.count; i++) {
let item = sourceModel.get(i)
if (item && item.sourceId === sourceId) {
applySourceAtIndex(i)
return
}
}
applySourceAtIndex(0)
}
function cycleSource(delta) {
if (sourceModel.count <= 0) return
let i = sourceList.currentIndex
@ -110,31 +97,9 @@ ColumnLayout {
Connections {
target: GamepadManager
function onNavigateUp() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (!sourceList.activeFocus) return
if (sourceList.count > 0 && sourceList.currentIndex <= 0) {
aboutAction.forceActiveFocus()
} else {
sourceList.decrementCurrentIndex()
}
}
function onNavigateDown() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (!sourceList.activeFocus) return
if (sourceList.count > 0 && sourceList.currentIndex >= sourceList.count - 1) {
importAction.forceActiveFocus()
} else {
sourceList.incrementCurrentIndex()
}
}
function onSelectPressed() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (sourceList.activeFocus) sidebarRoot.activateCurrentItem()
}
function onNavigateUp() { if (sourceList.activeFocus) sourceList.decrementCurrentIndex() }
function onNavigateDown() { if (sourceList.activeFocus) sourceList.incrementCurrentIndex() }
function onSelectPressed() { if (sourceList.activeFocus) sidebarRoot.activateCurrentItem() }
}
delegate: QQC2.ItemDelegate {
@ -329,8 +294,6 @@ ColumnLayout {
Connections {
target: GamepadManager
function onNavigateUp() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (aboutAction.activeFocus) {
settingsAction.forceActiveFocus()
} else if (settingsAction.activeFocus) {
@ -341,20 +304,13 @@ ColumnLayout {
}
}
function onNavigateDown() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (importAction.activeFocus) {
settingsAction.forceActiveFocus()
} else if (settingsAction.activeFocus) {
aboutAction.forceActiveFocus()
} else if (aboutAction.activeFocus) {
sourceList.forceActiveFocus()
sourceList.currentIndex = 0
}
}
function onSelectPressed() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (importAction.activeFocus) {
importAction.clicked()
} else if (settingsAction.activeFocus) {

View file

@ -7,19 +7,9 @@ import org.kde.alakarte
RowLayout {
id: root
spacing: uiMode === Config.Couch ? Kirigami.Units.largeSpacing * 1.25 : Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
property int uiMode: Config.Auto
property int activeInput: InputManager.KeyboardMouse
readonly property bool useGamepadHints: {
if (!GamepadManager.connected) return false
if (uiMode === Config.Couch) {
if (activeInput === InputManager.KeyboardMouse && InputManager.hasSeenKeyboardMouse) return false
return true
}
return activeInput === InputManager.Gamepad
}
readonly property bool useGamepadHints: GamepadManager.connected
readonly property int style: GamepadManager.controllerStyle
property string context: "library"
@ -59,9 +49,6 @@ RowLayout {
function iconForAux(action) {
if (!useGamepadHints) return ""
if (action === "dpad") {
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/dpad.svg"
}
if (action === "lb") {
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/lb.svg"
}
@ -85,25 +72,12 @@ RowLayout {
}
function keyboardLabel(action) {
if (root.context === "confirm" || root.context === "confirm_remove") {
switch (action) {
case "navigate": return "Tab"
case "confirm": return "Enter"
case "back": return "Esc"
case "menu": return ""
default: return ""
}
}
if (root.context === "library") {
switch (action) {
case "navigate": return i18n("Arrows")
case "confirm": return "Space"
case "back": return ""
case "back": return "Esc"
case "details": return "Enter"
case "search": return "Ctrl+F"
case "lb": return "Ctrl+PgUp"
case "rb": return "Ctrl+PgDown"
case "menu": return "Ctrl+,"
default: return ""
}
@ -111,7 +85,6 @@ RowLayout {
if (root.context === "edit") {
switch (action) {
case "navigate": return "Tab"
case "confirm": return "Enter"
case "back": return "Esc"
default: return ""
@ -120,11 +93,8 @@ RowLayout {
if (root.context === "details") {
switch (action) {
case "navigate": return "Tab"
case "confirm": return "Enter"
case "back": return "Esc"
case "details": return "F"
case "search": return "E"
case "menu": return "Ctrl+,"
default: return ""
}
@ -132,19 +102,6 @@ RowLayout {
if (root.context === "sidebar") {
switch (action) {
case "navigate": return i18n("Arrows/Tab")
case "confirm": return "Enter"
case "back": return "Esc"
case "lb": return "Ctrl+PgUp"
case "rb": return "Ctrl+PgDown"
case "menu": return "Ctrl+,"
default: return ""
}
}
if (root.context === "settings") {
switch (action) {
case "navigate": return "Tab"
case "confirm": return "Enter"
case "back": return "Esc"
case "menu": return "Ctrl+,"
@ -152,9 +109,8 @@ RowLayout {
}
}
if (root.context === "settings" || root.context === "import" || root.context === "sidebar" || root.context === "about" || root.context === "diagnostics") {
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
switch (action) {
case "navigate": return "Tab"
case "confirm": return "Enter"
case "back": return "Esc"
default: return ""
@ -165,100 +121,53 @@ RowLayout {
}
function actionLabel(action) {
if (root.context === "confirm" || root.context === "confirm_remove") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "back": return i18n("Cancel")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
default: return ""
}
}
if (root.context === "library") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Play")
case "back": return ""
case "back": return i18n("Back")
case "details": return i18n("Details")
case "search": return i18n("Search")
case "lb": return i18n("Prev Source")
case "rb": return i18n("Next Source")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Settings")
default: return ""
}
}
if (root.context === "settings") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "back": return i18n("Back")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Close")
default: return ""
}
}
if (root.context === "diagnostics") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "back": return i18n("Back")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Close")
case "menu": return i18n("Settings")
default: return ""
}
}
if (root.context === "edit") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "back": return i18n("Back")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
default: return ""
}
}
if (root.context === "details") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "confirm": return i18n("Play")
case "back": return i18n("Back")
case "details": return i18n("Favorite")
case "search": return i18n("Edit")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Settings")
case "menu": return i18n("Settings")
default: return ""
}
}
if (root.context === "sidebar") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "back": return i18n("Back")
case "lb": return i18n("Prev Source")
case "rb": return i18n("Next Source")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Close") : i18n("Settings")
case "menu": return i18n("Settings")
default: return ""
}
}
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "back": return i18n("Back")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
default: return ""
}
}
if (root.context === "about") {
switch (action) {
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
case "confirm": return i18n("Select")
case "back": return i18n("Back")
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
default: return ""
}
}
@ -286,7 +195,7 @@ RowLayout {
border.width: 1
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
Layout.preferredHeight: uiMode === Config.Couch ? Kirigami.Units.gridUnit * 1.8 : Kirigami.Units.gridUnit * 1.4
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.4
Layout.preferredWidth: useGamepadHints
? Layout.preferredHeight
: Math.max(keyText.implicitWidth + Kirigami.Units.mediumSpacing * 2, Layout.preferredHeight)
@ -306,7 +215,7 @@ RowLayout {
anchors.centerIn: parent
text: parent.parent.keyLabel
font.bold: true
font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize
font.pointSize: Kirigami.Theme.smallFont.pointSize
color: Kirigami.Theme.textColor
visible: !useGamepadHints
}
@ -315,18 +224,11 @@ RowLayout {
QQC2.Label {
text: parent.label
color: Kirigami.Theme.textColor
font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize
font.pointSize: Kirigami.Theme.smallFont.pointSize
Layout.alignment: Qt.AlignVCenter
}
}
HintItem {
action: "navigate"
label: actionLabel("navigate")
iconSource: iconForAux("dpad")
keyLabel: keyboardLabel("navigate")
}
HintItem {
action: "confirm"
label: actionLabel("confirm")
@ -359,22 +261,20 @@ RowLayout {
action: "lb"
label: actionLabel("lb")
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("lb") : ""
keyLabel: keyboardLabel("lb")
keyLabel: ""
}
HintItem {
action: "rb"
label: actionLabel("rb")
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("rb") : ""
keyLabel: keyboardLabel("rb")
keyLabel: ""
}
HintItem {
action: "menu"
label: actionLabel("menu")
iconSource: (useGamepadHints && uiMode === Config.Couch)
? iconForAux("menu")
: ((root.context === "library" || root.context === "details" || root.context === "sidebar" || root.context === "settings" || root.context === "diagnostics") ? iconForAux("menu") : "")
iconSource: (root.context === "library" || root.context === "details" || root.context === "sidebar") ? iconForAux("menu") : ""
keyLabel: keyboardLabel("menu")
}
}

View file

@ -74,29 +74,11 @@ GridView {
Connections {
target: GamepadManager
function onNavigateUp() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (gridView.activeFocus) gridView.navigateUp()
}
function onNavigateDown() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (gridView.activeFocus) gridView.navigateDown()
}
function onNavigateLeft() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (gridView.activeFocus) gridView.navigateLeft()
}
function onNavigateRight() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (gridView.activeFocus) gridView.navigateRight()
}
function onNavigateUp() { if (gridView.activeFocus) gridView.navigateUp() }
function onNavigateDown() { if (gridView.activeFocus) gridView.navigateDown() }
function onNavigateLeft() { if (gridView.activeFocus) gridView.navigateLeft() }
function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() }
function onSelectPressed() {
let w = applicationWindow()
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
if (!gridView.activeFocus) {
return
}

View file

@ -13,57 +13,6 @@ QQC2.ToolBar {
property alias searchField: searchFieldContainer.data
property int currentSortMode: 0
readonly property bool anyMenuOpen: sortMenu.visible
function closeCurrentMenu() {
if (sortMenu.visible) {
sortMenu.close()
}
}
function isDescendant(item, ancestor) {
let p = item
while (p) {
if (p === ancestor) return true
if (ancestor.contentItem && p === ancestor.contentItem) return true
if (p.visualParent !== undefined && p.visualParent !== null) {
if (searchHeader.isDescendant(p.visualParent, ancestor)) return true
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
if (searchHeader.isDescendant(p.popup.visualParent, ancestor)) return true
}
p = p.parent
}
return false
}
function focusNextInMenu(forward) {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let next = w.activeFocusItem
for (let i = 0; i < 50; i++) {
next = next.nextItemInFocusChain(forward)
if (!next) return
if (searchHeader.isDescendant(next, sortMenu)) {
next.forceActiveFocus()
return
}
}
}
function activateFocusedInMenu() {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let item = w.activeFocusItem
if (typeof item.triggered === "function") {
item.triggered()
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
signal searchChanged(string text)
signal sortChanged(int mode)
@ -98,12 +47,8 @@ QQC2.ToolBar {
QQC2.Menu {
id: sortMenu
focus: true
onOpened: Qt.callLater(function() { sortModeLastPlayed.forceActiveFocus() })
QQC2.MenuItem {
id: sortModeLastPlayed
text: i18n("Last Played")
checkable: true
checked: searchHeader.currentSortMode === 0
@ -114,7 +59,6 @@ QQC2.ToolBar {
}
QQC2.MenuItem {
id: sortModeName
text: i18n("Name")
checkable: true
checked: searchHeader.currentSortMode === 1
@ -125,7 +69,6 @@ QQC2.ToolBar {
}
QQC2.MenuItem {
id: sortModePlayTime
text: i18n("Play Time")
checkable: true
checked: searchHeader.currentSortMode === 2
@ -137,43 +80,4 @@ QQC2.ToolBar {
}
}
}
Connections {
target: GamepadManager
function onNavigateUp() {
if (!sortMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
searchHeader.focusNextInMenu(false)
}
function onNavigateDown() {
if (!sortMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
searchHeader.focusNextInMenu(true)
}
function onNavigateLeft() {
if (!sortMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
searchHeader.focusNextInMenu(false)
}
function onNavigateRight() {
if (!sortMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
searchHeader.focusNextInMenu(true)
}
function onSelectPressed() {
if (!sortMenu.visible) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
searchHeader.activateFocusedInMenu()
}
}
}