mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
Add Plasma tray, notification inhibition, session control, and flatpak runner
Expand TrayController with daemon health polling every 10 seconds via D-Bus Ping calls to org.kde.GameCenter1, org.kde.ALaKarte.Runner1, and org.kde.ALaKarte.Input1. Add menu actions to toggle console behaviors, notification mirroring, and to restart each daemon via systemctl --user. Add NotificationInhibitor, owned by App, which calls org.freedesktop.Notifications Inhibit/UnInhibit on the session bus whenever the consoleBehaviors config setting changes. The cookie is released on application quit. Add consoleBehaviors and mirrorNotifications properties to Config with KConfig persistence under the [Console] group. Ship org.kde.alakarte.notifyrc defining GameLaunched, GameExited, and LaunchFailed notification events so Plasma attributes them correctly in the notification history. Extend RunnerManagerDaemon::ResolveLaunch with a flatpak runner branch that constructs a flatpak run command, translates environment overrides to --env= arguments, and respects flatpakAppId, flatpakBranch, flatpakArch, and flatpakArgs from the launch spec. Add activate_session, switch_to_vt, and terminate_session actions to the gamecenter KAuth helper and its polkit policy. Each action calls the corresponding method on org.freedesktop.login1 over the system bus. Add CouchSidebar.qml, a horizontal source tab bar shown in couch mode above the library view, exposing the same sourceSelected, settingsRequested, importRequested, and aboutRequested signals as SidebarView. Fix duplicate adaptiveFocusRingWidth property in GameCard.qml.
This commit is contained in:
parent
0b10b01cc4
commit
e592e7b093
21 changed files with 1123 additions and 9 deletions
|
|
@ -44,6 +44,7 @@ find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS
|
|||
Quick
|
||||
QuickControls2
|
||||
Gui
|
||||
Widgets
|
||||
Network
|
||||
Concurrent
|
||||
DBus
|
||||
|
|
@ -108,6 +109,8 @@ find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
|
|||
|
||||
find_package(KF6Auth ${KF_MIN_VERSION} QUIET)
|
||||
|
||||
find_package(KF6StatusNotifierItem ${KF_MIN_VERSION} QUIET)
|
||||
|
||||
find_package(KF6KirigamiAddons 1.0.0 REQUIRED)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
add_executable(alakarte
|
||||
main.cpp
|
||||
app.cpp
|
||||
notificationinhibitor.cpp
|
||||
gamemodel.cpp
|
||||
gamesortfiltermodel.cpp
|
||||
game.cpp
|
||||
|
|
@ -68,6 +69,7 @@ target_link_libraries(alakarte PRIVATE
|
|||
Qt6::Quick
|
||||
Qt6::QuickControls2
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::Network
|
||||
Qt6::Sql
|
||||
Qt6::Concurrent
|
||||
|
|
@ -84,11 +86,25 @@ target_link_libraries(alakarte PRIVATE
|
|||
KF6::WindowSystem
|
||||
)
|
||||
|
||||
if (KF6StatusNotifierItem_FOUND)
|
||||
target_sources(alakarte PRIVATE
|
||||
traycontroller.cpp
|
||||
traycontroller.h
|
||||
)
|
||||
target_compile_definitions(alakarte PRIVATE
|
||||
ALAKARTE_HAVE_STATUSNOTIFIERITEM
|
||||
)
|
||||
target_link_libraries(alakarte PRIVATE
|
||||
KF6::StatusNotifierItem
|
||||
)
|
||||
endif()
|
||||
|
||||
ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
qml/Main.qml
|
||||
qml/LibraryView.qml
|
||||
qml/CouchSidebar.qml
|
||||
qml/GameCard.qml
|
||||
qml/GameDetailsSheet.qml
|
||||
qml/DiagnosticsSheet.qml
|
||||
|
|
@ -166,6 +182,10 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
|||
|
||||
install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
install(FILES org.kde.alakarte.notifyrc
|
||||
DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR}
|
||||
)
|
||||
|
||||
add_subdirectory(gamecenter)
|
||||
|
||||
add_subdirectory(runner)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "itchimporter.h"
|
||||
#include "legendaryimporter.h"
|
||||
#include "lutrisimporter.h"
|
||||
#include "notificationinhibitor.h"
|
||||
#include "retroarchimporter.h"
|
||||
#include "steamimporter.h"
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ App::App(QObject *parent)
|
|||
, m_steamGridDB(new SteamGridDB(this))
|
||||
, m_mediaManager(new MediaManager(this))
|
||||
, m_config(new Config(this))
|
||||
, m_notificationInhibitor(new NotificationInhibitor(m_config, this))
|
||||
{
|
||||
if (QCoreApplication::instance()) {
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
#include "runnermanagerclient.h"
|
||||
#include "steamgriddb.h"
|
||||
|
||||
class NotificationInhibitor;
|
||||
|
||||
class App : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -89,6 +91,7 @@ private:
|
|||
SteamGridDB *m_steamGridDB;
|
||||
MediaManager *m_mediaManager;
|
||||
Config *m_config;
|
||||
NotificationInhibitor *m_notificationInhibitor;
|
||||
|
||||
bool m_importing = false;
|
||||
QString m_importStatus;
|
||||
|
|
|
|||
|
|
@ -323,6 +323,34 @@ void Config::setWindowMaximized(bool maximized)
|
|||
}
|
||||
}
|
||||
|
||||
bool Config::consoleBehaviors() const
|
||||
{
|
||||
return m_consoleBehaviors;
|
||||
}
|
||||
|
||||
void Config::setConsoleBehaviors(bool enabled)
|
||||
{
|
||||
if (m_consoleBehaviors != enabled) {
|
||||
m_consoleBehaviors = enabled;
|
||||
save();
|
||||
Q_EMIT consoleBehaviorsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool Config::mirrorNotifications() const
|
||||
{
|
||||
return m_mirrorNotifications;
|
||||
}
|
||||
|
||||
void Config::setMirrorNotifications(bool mirror)
|
||||
{
|
||||
if (m_mirrorNotifications != mirror) {
|
||||
m_mirrorNotifications = mirror;
|
||||
save();
|
||||
Q_EMIT mirrorNotificationsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Config::setWindowSize(int width, int height)
|
||||
{
|
||||
width = qBound(0, width, 10000);
|
||||
|
|
@ -381,6 +409,10 @@ void Config::save()
|
|||
windowGroup.writeEntry(QStringLiteral("height"), m_windowHeight);
|
||||
windowGroup.writeEntry(QStringLiteral("maximized"), m_windowMaximized);
|
||||
|
||||
KConfigGroup consoleGroup(config, QStringLiteral("Console"));
|
||||
consoleGroup.writeEntry(QStringLiteral("consoleBehaviors"), m_consoleBehaviors);
|
||||
consoleGroup.writeEntry(QStringLiteral("mirrorNotifications"), m_mirrorNotifications);
|
||||
|
||||
config->sync();
|
||||
}
|
||||
|
||||
|
|
@ -417,6 +449,10 @@ void Config::load()
|
|||
m_windowWidth = windowGroup.readEntry(QStringLiteral("width"), 0);
|
||||
m_windowHeight = windowGroup.readEntry(QStringLiteral("height"), 0);
|
||||
m_windowMaximized = windowGroup.readEntry(QStringLiteral("maximized"), false);
|
||||
|
||||
const KConfigGroup consoleGroup(config, QStringLiteral("Console"));
|
||||
m_consoleBehaviors = consoleGroup.readEntry(QStringLiteral("consoleBehaviors"), false);
|
||||
m_mirrorNotifications = consoleGroup.readEntry(QStringLiteral("mirrorNotifications"), true);
|
||||
}
|
||||
|
||||
void Config::resetToDefaults()
|
||||
|
|
@ -444,6 +480,8 @@ void Config::resetToDefaults()
|
|||
m_windowWidth = 0;
|
||||
m_windowHeight = 0;
|
||||
m_windowMaximized = false;
|
||||
m_consoleBehaviors = false;
|
||||
m_mirrorNotifications = true;
|
||||
|
||||
save();
|
||||
|
||||
|
|
@ -469,4 +507,6 @@ void Config::resetToDefaults()
|
|||
Q_EMIT windowWidthChanged();
|
||||
Q_EMIT windowHeightChanged();
|
||||
Q_EMIT windowMaximizedChanged();
|
||||
Q_EMIT consoleBehaviorsChanged();
|
||||
Q_EMIT mirrorNotificationsChanged();
|
||||
}
|
||||
|
|
|
|||
12
src/config.h
12
src/config.h
|
|
@ -33,6 +33,8 @@ class Config : public QObject
|
|||
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)
|
||||
Q_PROPERTY(bool consoleBehaviors READ consoleBehaviors WRITE setConsoleBehaviors NOTIFY consoleBehaviorsChanged)
|
||||
Q_PROPERTY(bool mirrorNotifications READ mirrorNotifications WRITE setMirrorNotifications NOTIFY mirrorNotificationsChanged)
|
||||
|
||||
public:
|
||||
enum ViewMode {
|
||||
|
|
@ -117,6 +119,12 @@ public:
|
|||
bool windowMaximized() const;
|
||||
void setWindowMaximized(bool maximized);
|
||||
|
||||
bool consoleBehaviors() const;
|
||||
void setConsoleBehaviors(bool enabled);
|
||||
|
||||
bool mirrorNotifications() const;
|
||||
void setMirrorNotifications(bool mirror);
|
||||
|
||||
Q_INVOKABLE void save();
|
||||
Q_INVOKABLE void load();
|
||||
Q_INVOKABLE void resetToDefaults();
|
||||
|
|
@ -146,6 +154,8 @@ Q_SIGNALS:
|
|||
void windowWidthChanged();
|
||||
void windowHeightChanged();
|
||||
void windowMaximizedChanged();
|
||||
void consoleBehaviorsChanged();
|
||||
void mirrorNotificationsChanged();
|
||||
|
||||
private:
|
||||
ViewMode m_viewMode = GridView;
|
||||
|
|
@ -171,4 +181,6 @@ private:
|
|||
int m_windowWidth = 0;
|
||||
int m_windowHeight = 0;
|
||||
bool m_windowMaximized = false;
|
||||
bool m_consoleBehaviors = false;
|
||||
bool m_mirrorNotifications = true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,12 +22,21 @@ static const QString kPowerProfilesService = QStringLiteral("net.hadess.PowerPro
|
|||
static const QString kPowerProfilesPath = QStringLiteral("/net/hadess/PowerProfiles");
|
||||
static const QString kPowerProfilesInterface = QStringLiteral("net.hadess.PowerProfiles");
|
||||
|
||||
static const QString kLogin1Service = QStringLiteral("org.freedesktop.login1");
|
||||
static const QString kLogin1ManagerPath = QStringLiteral("/org/freedesktop/login1");
|
||||
static const QString kLogin1ManagerIface = QStringLiteral("org.freedesktop.login1.Manager");
|
||||
static const QString kLogin1SeatPath = QStringLiteral("/org/freedesktop/login1/seat/seat0");
|
||||
static const QString kLogin1SeatIface = QStringLiteral("org.freedesktop.login1.Seat");
|
||||
|
||||
class GameCenterHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public Q_SLOTS:
|
||||
ActionReply setpowerprofile(const QVariantMap &args);
|
||||
ActionReply activate_session(const QVariantMap &args);
|
||||
ActionReply switch_to_vt(const QVariantMap &args);
|
||||
ActionReply terminate_session(const QVariantMap &args);
|
||||
};
|
||||
|
||||
static QVariant unwrapDbusVariant(QVariant v)
|
||||
|
|
@ -128,6 +137,110 @@ ActionReply GameCenterHelper::setpowerprofile(const QVariantMap &args)
|
|||
return ActionReply::SuccessReply();
|
||||
}
|
||||
|
||||
ActionReply GameCenterHelper::activate_session(const QVariantMap &args)
|
||||
{
|
||||
const QString sessionId = unwrapStringArg(args, QStringLiteral("sessionId"));
|
||||
if (sessionId.isEmpty()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("missing sessionId"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
QDBusConnection bus = QDBusConnection::systemBus();
|
||||
if (!bus.isConnected()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("system bus not connected"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
QDBusInterface mgr(kLogin1Service, kLogin1ManagerPath, kLogin1ManagerIface, bus);
|
||||
if (!mgr.isValid()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("login1 manager interface not available"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
const QDBusMessage replyMsg = mgr.call(QStringLiteral("ActivateSession"), sessionId);
|
||||
if (replyMsg.type() == QDBusMessage::ErrorMessage) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(replyMsg.errorName() + QStringLiteral(": ") + replyMsg.errorMessage());
|
||||
return reply;
|
||||
}
|
||||
|
||||
return ActionReply::SuccessReply();
|
||||
}
|
||||
|
||||
ActionReply GameCenterHelper::switch_to_vt(const QVariantMap &args)
|
||||
{
|
||||
QVariant vtArg = args.value(QStringLiteral("vtnr"));
|
||||
if (vtArg.canConvert<QDBusVariant>()) {
|
||||
vtArg = vtArg.value<QDBusVariant>().variant();
|
||||
}
|
||||
bool ok = false;
|
||||
const uint vtnr = vtArg.toUInt(&ok);
|
||||
if (!ok || vtnr == 0) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("missing or invalid vtnr"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
QDBusConnection bus = QDBusConnection::systemBus();
|
||||
if (!bus.isConnected()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("system bus not connected"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
QDBusInterface seat(kLogin1Service, kLogin1SeatPath, kLogin1SeatIface, bus);
|
||||
if (!seat.isValid()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("login1 seat interface not available"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
const QDBusMessage replyMsg = seat.call(QStringLiteral("SwitchTo"), vtnr);
|
||||
if (replyMsg.type() == QDBusMessage::ErrorMessage) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(replyMsg.errorName() + QStringLiteral(": ") + replyMsg.errorMessage());
|
||||
return reply;
|
||||
}
|
||||
|
||||
return ActionReply::SuccessReply();
|
||||
}
|
||||
|
||||
ActionReply GameCenterHelper::terminate_session(const QVariantMap &args)
|
||||
{
|
||||
const QString sessionId = unwrapStringArg(args, QStringLiteral("sessionId"));
|
||||
if (sessionId.isEmpty()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("missing sessionId"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
QDBusConnection bus = QDBusConnection::systemBus();
|
||||
if (!bus.isConnected()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("system bus not connected"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
QDBusInterface mgr(kLogin1Service, kLogin1ManagerPath, kLogin1ManagerIface, bus);
|
||||
if (!mgr.isValid()) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(QStringLiteral("login1 manager interface not available"));
|
||||
return reply;
|
||||
}
|
||||
|
||||
const QDBusMessage replyMsg = mgr.call(QStringLiteral("TerminateSession"), sessionId);
|
||||
if (replyMsg.type() == QDBusMessage::ErrorMessage) {
|
||||
ActionReply reply = ActionReply::HelperErrorReply();
|
||||
reply.setErrorDescription(replyMsg.errorName() + QStringLiteral(": ") + replyMsg.errorMessage());
|
||||
return reply;
|
||||
}
|
||||
|
||||
return ActionReply::SuccessReply();
|
||||
}
|
||||
|
||||
KAUTH_HELPER_MAIN("org.kde.alakarte.gamecenter.helper", GameCenterHelper)
|
||||
|
||||
#include "gamecenterkauthhelper.moc"
|
||||
|
|
|
|||
|
|
@ -14,4 +14,28 @@
|
|||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
<action id="org.kde.alakarte.gamecenter.helper.activate_session">
|
||||
<description>Activate a login session</description>
|
||||
<message>Authentication is required to activate a login session.</message>
|
||||
<defaults>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
<action id="org.kde.alakarte.gamecenter.helper.switch_to_vt">
|
||||
<description>Switch to a virtual terminal</description>
|
||||
<message>Authentication is required to switch to a virtual terminal.</message>
|
||||
<defaults>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
<action id="org.kde.alakarte.gamecenter.helper.terminate_session">
|
||||
<description>Terminate a login session</description>
|
||||
<message>Authentication is required to terminate a login session.</message>
|
||||
<defaults>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
</policyconfig>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusError>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusVariant>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
|
|
@ -32,7 +33,6 @@ static bool pingDaemon(QDBusConnection bus)
|
|||
if (!bus.isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, bus);
|
||||
if (!iface.isValid()) {
|
||||
return false;
|
||||
|
|
@ -283,7 +283,14 @@ GameLauncher::GameLauncher(QObject *parent)
|
|||
});
|
||||
}
|
||||
|
||||
if (QCoreApplication::instance()) {
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||
shutdownSpawnedDaemon();
|
||||
});
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
ensureDaemon();
|
||||
checkDaemonAvailability();
|
||||
syncDaemonSessions();
|
||||
});
|
||||
|
|
@ -291,10 +298,97 @@ GameLauncher::GameLauncher(QObject *parent)
|
|||
|
||||
GameLauncher::~GameLauncher()
|
||||
{
|
||||
shutdownSpawnedDaemon();
|
||||
m_daemonGameToSession.clear();
|
||||
m_daemonSessionToGame.clear();
|
||||
}
|
||||
|
||||
void GameLauncher::ensureDaemon()
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (!bus.isConnected() || !bus.interface()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bus.interface()->isServiceRegistered(kGameCenterService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool activatable = false;
|
||||
{
|
||||
const QDBusReply<QStringList> names = bus.interface()->activatableServiceNames();
|
||||
if (names.isValid()) {
|
||||
activatable = names.value().contains(kGameCenterService);
|
||||
}
|
||||
}
|
||||
|
||||
bus.interface()->startService(kGameCenterService);
|
||||
if (bus.interface()->isServiceRegistered(kGameCenterService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activatable) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(QT_NO_DEBUG)
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (m_daemonSpawnAttempted) {
|
||||
return;
|
||||
}
|
||||
m_daemonSpawnAttempted = true;
|
||||
|
||||
QString program = QCoreApplication::applicationDirPath() + QLatin1String("/alakarte-gamecenter");
|
||||
if (!QFileInfo::exists(program)) {
|
||||
program = QStringLiteral("alakarte-gamecenter");
|
||||
}
|
||||
|
||||
if (!m_daemonProcess) {
|
||||
m_daemonProcess = new QProcess(this);
|
||||
m_daemonProcess->setProcessChannelMode(QProcess::MergedChannels);
|
||||
connect(m_daemonProcess, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
|
||||
if (!m_daemonProcess) {
|
||||
return;
|
||||
}
|
||||
m_daemonProcess->deleteLater();
|
||||
m_daemonProcess = nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
m_daemonProcess->setProgram(program);
|
||||
m_daemonProcess->setArguments({});
|
||||
m_daemonProcess->start();
|
||||
|
||||
QTimer::singleShot(500, this, [this]() {
|
||||
checkDaemonAvailability();
|
||||
if (m_daemonAvailable) {
|
||||
syncDaemonSessions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void GameLauncher::shutdownSpawnedDaemon()
|
||||
{
|
||||
if (!m_daemonProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
QProcess *p = m_daemonProcess;
|
||||
if (p->state() != QProcess::NotRunning) {
|
||||
p->disconnect(this);
|
||||
p->terminate();
|
||||
if (!p->waitForFinished(3000)) {
|
||||
p->kill();
|
||||
p->waitForFinished(3000);
|
||||
}
|
||||
}
|
||||
|
||||
p->deleteLater();
|
||||
m_daemonProcess = nullptr;
|
||||
}
|
||||
|
||||
bool GameLauncher::hasRunningGames() const
|
||||
{
|
||||
return !m_daemonGameToSession.isEmpty();
|
||||
|
|
@ -803,6 +897,7 @@ void GameLauncher::applyRunningStateToLibrary()
|
|||
|
||||
void GameLauncher::checkDaemonAvailability()
|
||||
{
|
||||
ensureDaemon();
|
||||
const bool available = pingDaemon(QDBusConnection::sessionBus());
|
||||
|
||||
disconnectDaemonSignals(QDBusConnection::sessionBus(), this);
|
||||
|
|
|
|||
|
|
@ -49,7 +49,12 @@ private:
|
|||
QHash<QString, QString> m_daemonSessionToGame;
|
||||
bool m_daemonAvailable = false;
|
||||
|
||||
bool m_daemonSpawnAttempted = false;
|
||||
class QProcess *m_daemonProcess = nullptr;
|
||||
|
||||
void checkDaemonAvailability();
|
||||
void ensureDaemon();
|
||||
void shutdownSpawnedDaemon();
|
||||
void syncDaemonSessions();
|
||||
void applyRunningStateToLibrary();
|
||||
};
|
||||
|
|
|
|||
58
src/main.cpp
58
src/main.cpp
|
|
@ -1,6 +1,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QCommandLineParser>
|
||||
#include <QGuiApplication>
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
#include <QWindow>
|
||||
|
||||
#include <KAboutData>
|
||||
#include <KCrash>
|
||||
|
|
@ -16,11 +18,22 @@
|
|||
#include <KLocalizedString>
|
||||
|
||||
#include "alakarte-version.h"
|
||||
#include "app.h"
|
||||
#ifdef ALAKARTE_HAVE_STATUSNOTIFIERITEM
|
||||
#include "traycontroller.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_UNIX)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
qputenv("QT_DESKTOP_FILE_NAME", QByteArray("org.kde.alakarte"));
|
||||
QGuiApplication app(argc, argv);
|
||||
QApplication app(argc, argv);
|
||||
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
|
||||
|
||||
app.setOrganizationDomain(QStringLiteral("kde.org"));
|
||||
|
|
@ -50,9 +63,42 @@ int main(int argc, char *argv[])
|
|||
|
||||
QCommandLineParser parser;
|
||||
aboutData.setupCommandLine(&parser);
|
||||
|
||||
const QCommandLineOption quitOnCloseOption(QStringLiteral("quit-on-close"), i18n("Quit when the main window is closed"));
|
||||
const QCommandLineOption closeToTrayOption(QStringLiteral("close-to-tray"), i18n("Hide the main window to the system tray when closed"));
|
||||
parser.addOption(quitOnCloseOption);
|
||||
parser.addOption(closeToTrayOption);
|
||||
|
||||
parser.process(app);
|
||||
aboutData.processCommandLine(&parser);
|
||||
|
||||
bool terminalLaunched = false;
|
||||
#if defined(Q_OS_UNIX)
|
||||
terminalLaunched = (isatty(STDIN_FILENO) || isatty(STDOUT_FILENO) || isatty(STDERR_FILENO));
|
||||
#endif
|
||||
|
||||
bool closeToTray = false;
|
||||
#ifdef ALAKARTE_HAVE_STATUSNOTIFIERITEM
|
||||
bool trayHostAvailable = false;
|
||||
{
|
||||
auto bus = QDBusConnection::sessionBus();
|
||||
if (bus.isConnected() && bus.interface()) {
|
||||
trayHostAvailable = bus.interface()->isServiceRegistered(QStringLiteral("org.kde.StatusNotifierWatcher"))
|
||||
|| bus.interface()->isServiceRegistered(QStringLiteral("org.freedesktop.StatusNotifierWatcher"));
|
||||
}
|
||||
}
|
||||
|
||||
closeToTray = (!terminalLaunched) && trayHostAvailable;
|
||||
if (parser.isSet(quitOnCloseOption)) {
|
||||
closeToTray = false;
|
||||
}
|
||||
if (parser.isSet(closeToTrayOption)) {
|
||||
closeToTray = trayHostAvailable;
|
||||
}
|
||||
#endif
|
||||
|
||||
QApplication::setQuitOnLastWindowClosed(!closeToTray);
|
||||
|
||||
KDBusService service(KDBusService::Unique);
|
||||
|
||||
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||
|
|
@ -69,5 +115,15 @@ int main(int argc, char *argv[])
|
|||
return -1;
|
||||
}
|
||||
|
||||
auto *window = qobject_cast<QWindow *>(engine.rootObjects().constFirst());
|
||||
if (window) {
|
||||
#ifdef ALAKARTE_HAVE_STATUSNOTIFIERITEM
|
||||
if (closeToTray) {
|
||||
Config *cfg = App::instance() ? App::instance()->config() : nullptr;
|
||||
new TrayController(window, true, cfg, &app);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
|
|
|||
88
src/notificationinhibitor.cpp
Normal file
88
src/notificationinhibitor.cpp
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#include "notificationinhibitor.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusPendingReply>
|
||||
|
||||
static const QString kNotifService = QStringLiteral("org.freedesktop.Notifications");
|
||||
static const QString kNotifPath = QStringLiteral("/org/freedesktop/Notifications");
|
||||
static const QString kNotifIface = QStringLiteral("org.freedesktop.Notifications");
|
||||
|
||||
NotificationInhibitor::NotificationInhibitor(Config *config, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_config(config)
|
||||
{
|
||||
if (m_config) {
|
||||
connect(m_config, &Config::consoleBehaviorsChanged, this, [this]() {
|
||||
applyInhibition(m_config->consoleBehaviors());
|
||||
});
|
||||
|
||||
applyInhibition(m_config->consoleBehaviors());
|
||||
}
|
||||
|
||||
if (QCoreApplication::instance()) {
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||
if (m_inhibited) {
|
||||
uninhibit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NotificationInhibitor::~NotificationInhibitor()
|
||||
{
|
||||
if (m_inhibited) {
|
||||
uninhibit();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationInhibitor::applyInhibition(bool enabled)
|
||||
{
|
||||
if (enabled && !m_inhibited) {
|
||||
inhibit();
|
||||
} else if (!enabled && m_inhibited) {
|
||||
uninhibit();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationInhibitor::inhibit()
|
||||
{
|
||||
QDBusInterface ifc(kNotifService, kNotifPath, kNotifIface, QDBusConnection::sessionBus());
|
||||
if (!ifc.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = ifc.asyncCall(QStringLiteral("Inhibit"), QStringLiteral("org.kde.alakarte"), QStringLiteral("console behaviors enabled"), QVariantMap{});
|
||||
|
||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher](QDBusPendingCallWatcher *w) {
|
||||
QDBusPendingReply<uint> reply = *w;
|
||||
if (!reply.isError()) {
|
||||
m_cookie = reply.value();
|
||||
m_inhibited = true;
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void NotificationInhibitor::uninhibit()
|
||||
{
|
||||
if (!m_inhibited || m_cookie == 0) {
|
||||
m_inhibited = false;
|
||||
m_cookie = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
QDBusInterface ifc(kNotifService, kNotifPath, kNotifIface, QDBusConnection::sessionBus());
|
||||
if (ifc.isValid()) {
|
||||
ifc.asyncCall(QStringLiteral("UnInhibit"), m_cookie);
|
||||
}
|
||||
|
||||
m_inhibited = false;
|
||||
m_cookie = 0;
|
||||
}
|
||||
23
src/notificationinhibitor.h
Normal file
23
src/notificationinhibitor.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Config;
|
||||
|
||||
class NotificationInhibitor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NotificationInhibitor(Config *config, QObject *parent = nullptr);
|
||||
~NotificationInhibitor() override;
|
||||
|
||||
private:
|
||||
void applyInhibition(bool enabled);
|
||||
void inhibit();
|
||||
void uninhibit();
|
||||
|
||||
Config *m_config = nullptr;
|
||||
uint m_cookie = 0;
|
||||
bool m_inhibited = false;
|
||||
};
|
||||
21
src/org.kde.alakarte.notifyrc
Normal file
21
src/org.kde.alakarte.notifyrc
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[Global]
|
||||
IconName=org.kde.alakarte
|
||||
DesktopEntry=org.kde.alakarte
|
||||
|
||||
[Event/GameLaunched]
|
||||
Name=Game Launched
|
||||
Comment=A game session was started
|
||||
Action=Popup
|
||||
Urgency=Low
|
||||
|
||||
[Event/GameExited]
|
||||
Name=Game Exited
|
||||
Comment=A game session ended
|
||||
Action=Popup
|
||||
Urgency=Low
|
||||
|
||||
[Event/LaunchFailed]
|
||||
Name=Launch Failed
|
||||
Comment=A game failed to launch
|
||||
Action=Popup
|
||||
Urgency=Normal
|
||||
179
src/qml/CouchSidebar.qml
Normal file
179
src/qml/CouchSidebar.qml
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.alakarte
|
||||
import "components"
|
||||
|
||||
Item {
|
||||
id: couchSidebarRoot
|
||||
|
||||
property string currentSource: "all"
|
||||
|
||||
signal sourceSelected(string source)
|
||||
signal settingsRequested()
|
||||
signal importRequested()
|
||||
signal aboutRequested()
|
||||
|
||||
function iconInfoForPlatform(platformName) {
|
||||
let p = (platformName || "").toLowerCase()
|
||||
if (p.includes("steam")) return { source: "com.valvesoftware.Steam", fallback: "steam" }
|
||||
if (p.includes("itch")) return { source: "io.itch.itch", fallback: "applications-games" }
|
||||
if (p.includes("retroarch")) return { source: "org.libretro.RetroArch", fallback: "retroarch" }
|
||||
if (p.includes("lutris")) return { source: "lutris", fallback: "applications-games" }
|
||||
if (p.includes("heroic")) return { source: "com.heroicgameslauncher.hgl", fallback: "applications-games" }
|
||||
if (p.includes("bottles")) return { source: "com.usebottles.bottles", fallback: "application-x-executable" }
|
||||
if (p.includes("flatpak")) return { source: "flatpak-discover", fallback: "applications-games" }
|
||||
if (p.includes("desktop")) return { source: "user-desktop", fallback: "computer" }
|
||||
if (p.includes("legendary")) return { source: "legendary", fallback: "applications-games" }
|
||||
return { source: "applications-games", fallback: "applications-games" }
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: sourceModel
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
function refresh() {
|
||||
clear()
|
||||
let allCount = 0
|
||||
let favCount = 0
|
||||
let sources = {}
|
||||
|
||||
for (let i = 0; i < App.gameModel.rowCount(); i++) {
|
||||
let game = App.gameModel.gameAt(i)
|
||||
if (!game || game.hidden) continue
|
||||
allCount++
|
||||
if (game.favorite) favCount++
|
||||
let p = game.platform
|
||||
sources[p] = (sources[p] || 0) + 1
|
||||
}
|
||||
|
||||
append({ name: i18n("All"), sourceId: "all", icon: "view-list-icons", fallback: "applications-games" })
|
||||
if (favCount > 0) {
|
||||
append({ name: i18n("Favorites"), sourceId: "favorites", icon: "bookmark-new", fallback: "bookmark-new" })
|
||||
}
|
||||
for (let platform in sources) {
|
||||
let info = couchSidebarRoot.iconInfoForPlatform(platform)
|
||||
append({ name: platform, sourceId: platform, icon: info.source, fallback: info.fallback })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: App.gameModel
|
||||
function onCountChanged() { sourceModel.refresh() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: App
|
||||
function onImportCompleted() { sourceModel.refresh() }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
QQC2.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: tabList
|
||||
model: sourceModel
|
||||
orientation: ListView.Horizontal
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
leftMargin: Kirigami.Units.largeSpacing
|
||||
rightMargin: Kirigami.Units.largeSpacing
|
||||
topMargin: Kirigami.Units.smallSpacing
|
||||
bottomMargin: Kirigami.Units.smallSpacing
|
||||
clip: true
|
||||
|
||||
delegate: QQC2.ItemDelegate {
|
||||
id: tabDelegate
|
||||
width: implicitWidth
|
||||
height: tabList.height - tabList.topMargin - tabList.bottomMargin
|
||||
|
||||
readonly property bool isActive: model.sourceId === couchSidebarRoot.currentSource
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: tabDelegate.isActive ? Kirigami.Theme.highlightColor
|
||||
: tabDelegate.hovered ? Kirigami.Theme.alternateBackgroundColor
|
||||
: "transparent"
|
||||
opacity: tabDelegate.isActive ? 0.20 : (tabDelegate.hovered ? 0.10 : 0.0)
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2
|
||||
radius: 1
|
||||
color: Kirigami.Theme.highlightColor
|
||||
visible: tabDelegate.isActive
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
leftPadding: Kirigami.Units.mediumSpacing
|
||||
rightPadding: Kirigami.Units.mediumSpacing
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
source: model.icon
|
||||
fallback: model.fallback
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
color: tabDelegate.isActive ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: model.name
|
||||
color: tabDelegate.isActive ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||
font.bold: tabDelegate.isActive
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: couchSidebarRoot.sourceSelected(model.sourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "document-import"
|
||||
onClicked: couchSidebarRoot.importRequested()
|
||||
Layout.fillHeight: true
|
||||
flat: true
|
||||
QQC2.ToolTip.text: i18n("Import Games")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "configure"
|
||||
onClicked: couchSidebarRoot.settingsRequested()
|
||||
Layout.fillHeight: true
|
||||
flat: true
|
||||
QQC2.ToolTip.text: i18n("Settings")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,9 +20,9 @@ FocusScope {
|
|||
if (w && w.isTouchDevice !== undefined) return w.isTouchDevice
|
||||
return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
|
||||
}
|
||||
readonly property int adaptiveFocusRingWidth: 1
|
||||
readonly property real adaptiveHoverScale: 1.015
|
||||
readonly property real adaptiveFocusScale: 1.03
|
||||
readonly property real adaptiveHoverScale: root.isCouchMode ? 1.05 : 1.015
|
||||
readonly property real adaptiveFocusScale: root.isCouchMode ? 1.12 : 1.03
|
||||
readonly property int adaptiveFocusRingWidth: root.isCouchMode ? 3 : 1
|
||||
|
||||
readonly property bool useAnimatedCover: App.config.animatedCovers
|
||||
&& game
|
||||
|
|
@ -43,8 +43,8 @@ FocusScope {
|
|||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
shadow {
|
||||
size: gameCard.focused ? Kirigami.Units.mediumSpacing : (hoverHandler.hovered ? Kirigami.Units.smallSpacing * 1.5 : Kirigami.Units.smallSpacing)
|
||||
color: gameCard.focused ? Qt.rgba(0, 0, 0, 0.34) : (hoverHandler.hovered ? Qt.rgba(0, 0, 0, 0.24) : Qt.rgba(0, 0, 0, 0.16))
|
||||
size: gameCard.focused ? (root.isCouchMode ? Kirigami.Units.gridUnit * 1.5 : Kirigami.Units.mediumSpacing) : (hoverHandler.hovered ? Kirigami.Units.smallSpacing * 1.5 : Kirigami.Units.smallSpacing)
|
||||
color: gameCard.focused ? (root.isCouchMode ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.4) : Qt.rgba(0, 0, 0, 0.34)) : (hoverHandler.hovered ? Qt.rgba(0, 0, 0, 0.24) : Qt.rgba(0, 0, 0, 0.16))
|
||||
}
|
||||
|
||||
Behavior on shadow.color {
|
||||
|
|
@ -162,6 +162,8 @@ FocusScope {
|
|||
|
||||
// Gradient overlay for text
|
||||
Rectangle {
|
||||
opacity: (!root.isCouchMode || gameCard.focused || hoverHandler.hovered) ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
|
@ -178,6 +180,8 @@ FocusScope {
|
|||
|
||||
// Game title
|
||||
ColumnLayout {
|
||||
opacity: (!root.isCouchMode || gameCard.focused || hoverHandler.hovered) ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
|
|
|||
|
|
@ -161,6 +161,11 @@ FocusScope {
|
|||
Layout.fillWidth: true
|
||||
visible: libraryRoot.searchActive
|
||||
|
||||
// In Couch Mode, position it absolute over the grid instead of shifting the grid
|
||||
// to mimic a console drop-down search bar
|
||||
y: root.isCouchMode ? Kirigami.Units.gridUnit * 2 : 0
|
||||
z: 100
|
||||
|
||||
searchField: searchField
|
||||
|
||||
onSearchChanged: function(text) {
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ Kirigami.ApplicationWindow {
|
|||
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
|
||||
|
||||
footer: Item {
|
||||
visible: !root.isCouchMode
|
||||
implicitWidth: root.width
|
||||
|
||||
implicitHeight: footerBar.implicitHeight
|
||||
|
|
@ -749,6 +750,7 @@ Kirigami.ApplicationWindow {
|
|||
id: sidebar
|
||||
edge: Qt.LeftEdge
|
||||
modal: root.isMobile || root.isNarrowScreen || root.isCouchMode
|
||||
enabled: !root.isCouchMode
|
||||
handleClosedIcon.source: "application-menu"
|
||||
handleOpenIcon.source: "go-previous"
|
||||
handleVisible: root.isMobile || root.isNarrowScreen || root.isCouchMode
|
||||
|
|
@ -856,7 +858,37 @@ Kirigami.ApplicationWindow {
|
|||
id: mainPage
|
||||
title: sidebarView.currentSourceName
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
padding: root.isCouchMode ? 0 : Kirigami.Units.largeSpacing
|
||||
|
||||
// Couch Mode Dynamic Background
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.isCouchMode
|
||||
z: -1
|
||||
|
||||
Image {
|
||||
id: globalBackgroundCover
|
||||
anchors.fill: parent
|
||||
source: libraryView.focusedCoverUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: App.config.highQualityImages
|
||||
opacity: 0.15
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 0.95
|
||||
blurMax: 80
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.8)
|
||||
}
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
|
|
@ -900,6 +932,29 @@ Kirigami.ApplicationWindow {
|
|||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
CouchSidebar {
|
||||
id: couchSidebar
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 4
|
||||
visible: root.isCouchMode
|
||||
|
||||
currentSource: sidebarView.currentSource
|
||||
|
||||
onSourceSelected: function(source) {
|
||||
sidebarView.applySourceById(source)
|
||||
}
|
||||
|
||||
onSettingsRequested: {
|
||||
root.openSettings()
|
||||
}
|
||||
onImportRequested: {
|
||||
importSheet.open()
|
||||
}
|
||||
onAboutRequested: {
|
||||
aboutSheet.open()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
id: daemonBanner
|
||||
Layout.fillWidth: true
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,49 @@ QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
|
|||
finalProgram = protonExe;
|
||||
finalArgs = {QStringLiteral("run"), program};
|
||||
finalArgs.append(args);
|
||||
} else if (runner == QLatin1String("flatpak")) {
|
||||
const QString flatpakExe = QStandardPaths::findExecutable(QStringLiteral("flatpak"));
|
||||
if (flatpakExe.isEmpty()) {
|
||||
out.insert(QStringLiteral("error"), QStringLiteral("flatpak not found"));
|
||||
return out;
|
||||
}
|
||||
|
||||
const QString flatpakAppId = spec.value(QStringLiteral("flatpakAppId")).toString();
|
||||
const QString appId = !flatpakAppId.isEmpty() ? flatpakAppId : program;
|
||||
|
||||
const QString flatpakBranch = spec.value(QStringLiteral("flatpakBranch")).toString();
|
||||
const QString flatpakArch = spec.value(QStringLiteral("flatpakArch")).toString();
|
||||
const QStringList flatpakExtraArgs = unwrapStringList(spec.value(QStringLiteral("flatpakArgs")));
|
||||
|
||||
QStringList runArgs;
|
||||
runArgs.push_back(QStringLiteral("run"));
|
||||
|
||||
if (!flatpakBranch.isEmpty()) {
|
||||
runArgs.push_back(QStringLiteral("--branch=") + flatpakBranch);
|
||||
}
|
||||
if (!flatpakArch.isEmpty()) {
|
||||
runArgs.push_back(QStringLiteral("--arch=") + flatpakArch);
|
||||
}
|
||||
|
||||
for (auto it = effectiveEnv.constBegin(); it != effectiveEnv.constEnd(); ++it) {
|
||||
const QString val = it.value().toString();
|
||||
if (!val.isEmpty()) {
|
||||
runArgs.push_back(QStringLiteral("--env=") + it.key() + QLatin1Char('=') + val);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &a : flatpakExtraArgs) {
|
||||
if (!a.trimmed().isEmpty()) {
|
||||
runArgs.push_back(a);
|
||||
}
|
||||
}
|
||||
|
||||
runArgs.push_back(appId);
|
||||
runArgs.append(args);
|
||||
|
||||
finalProgram = flatpakExe;
|
||||
finalArgs = runArgs;
|
||||
effectiveEnv.clear();
|
||||
}
|
||||
|
||||
out.insert(QStringLiteral("finalProgram"), finalProgram);
|
||||
|
|
|
|||
266
src/traycontroller.cpp
Normal file
266
src/traycontroller.cpp
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
#include "traycontroller.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusPendingReply>
|
||||
#include <QEvent>
|
||||
#include <QMenu>
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
#include <QWindow>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <KStatusNotifierItem>
|
||||
|
||||
static constexpr int kHealthPollIntervalMs = 10000;
|
||||
|
||||
TrayController::TrayController(QWindow *window, bool closeToTray, Config *config, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_window(window)
|
||||
, m_config(config)
|
||||
{
|
||||
m_closeToTray = closeToTray;
|
||||
|
||||
if (m_window) {
|
||||
m_window->installEventFilter(this);
|
||||
}
|
||||
|
||||
m_sni = new KStatusNotifierItem(QStringLiteral("org.kde.alakarte"), this);
|
||||
m_sni->setTitle(QStringLiteral("A-La-Karte"));
|
||||
m_sni->setIconByName(QStringLiteral("org.kde.alakarte"));
|
||||
m_sni->setStatus(KStatusNotifierItem::Active);
|
||||
|
||||
if (m_window) {
|
||||
m_sni->setAssociatedWindow(m_window);
|
||||
}
|
||||
|
||||
m_menu = new QMenu();
|
||||
rebuildMenu();
|
||||
m_sni->setContextMenu(m_menu);
|
||||
|
||||
connect(m_sni, &KStatusNotifierItem::activateRequested, this, [this](bool, const QPoint &) {
|
||||
toggleWindow();
|
||||
});
|
||||
|
||||
if (m_config) {
|
||||
connect(m_config, &Config::consoleBehaviorsChanged, this, &TrayController::updateConsoleModeAction);
|
||||
connect(m_config, &Config::mirrorNotificationsChanged, this, &TrayController::updateMirrorNotifAction);
|
||||
}
|
||||
|
||||
m_healthTimer = new QTimer(this);
|
||||
m_healthTimer->setInterval(kHealthPollIntervalMs);
|
||||
connect(m_healthTimer, &QTimer::timeout, this, &TrayController::checkDaemonHealth);
|
||||
m_healthTimer->start();
|
||||
checkDaemonHealth();
|
||||
}
|
||||
|
||||
TrayController::~TrayController()
|
||||
{
|
||||
if (m_menu) {
|
||||
delete m_menu;
|
||||
m_menu = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool TrayController::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == m_window && event && event->type() == QEvent::Close) {
|
||||
if (m_closeToTray && !m_quitting) {
|
||||
event->setAccepted(false);
|
||||
hideWindow();
|
||||
}
|
||||
}
|
||||
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void TrayController::rebuildMenu()
|
||||
{
|
||||
m_menu->clear();
|
||||
|
||||
m_consoleModeAction = m_menu->addAction(i18n("Console Behaviors"));
|
||||
m_consoleModeAction->setCheckable(true);
|
||||
if (m_config) {
|
||||
m_consoleModeAction->setChecked(m_config->consoleBehaviors());
|
||||
}
|
||||
connect(m_consoleModeAction, &QAction::triggered, this, [this](bool checked) {
|
||||
if (m_config) {
|
||||
m_config->setConsoleBehaviors(checked);
|
||||
}
|
||||
});
|
||||
|
||||
m_mirrorNotifAction = m_menu->addAction(i18n("Mirror Notifications"));
|
||||
m_mirrorNotifAction->setCheckable(true);
|
||||
if (m_config) {
|
||||
m_mirrorNotifAction->setChecked(m_config->mirrorNotifications());
|
||||
}
|
||||
connect(m_mirrorNotifAction, &QAction::triggered, this, [this](bool checked) {
|
||||
if (m_config) {
|
||||
m_config->setMirrorNotifications(checked);
|
||||
}
|
||||
});
|
||||
|
||||
m_menu->addSeparator();
|
||||
|
||||
m_gameCenterStatusAction = m_menu->addAction(i18n("GameCenter: checking…"));
|
||||
m_gameCenterStatusAction->setEnabled(false);
|
||||
|
||||
m_runnerStatusAction = m_menu->addAction(i18n("Runner: checking…"));
|
||||
m_runnerStatusAction->setEnabled(false);
|
||||
|
||||
m_inputStatusAction = m_menu->addAction(i18n("Input: checking…"));
|
||||
m_inputStatusAction->setEnabled(false);
|
||||
|
||||
m_menu->addSeparator();
|
||||
|
||||
QMenu *restartMenu = m_menu->addMenu(i18n("Restart Daemons"));
|
||||
QAction *restartGC = restartMenu->addAction(i18n("Restart GameCenter"));
|
||||
connect(restartGC, &QAction::triggered, this, [this]() {
|
||||
restartDaemon(QStringLiteral("org.kde.GameCenter1.service"));
|
||||
});
|
||||
QAction *restartRunner = restartMenu->addAction(i18n("Restart Runner"));
|
||||
connect(restartRunner, &QAction::triggered, this, [this]() {
|
||||
restartDaemon(QStringLiteral("org.kde.ALaKarte.Runner1.service"));
|
||||
});
|
||||
QAction *restartInput = restartMenu->addAction(i18n("Restart Input"));
|
||||
connect(restartInput, &QAction::triggered, this, [this]() {
|
||||
restartDaemon(QStringLiteral("org.kde.ALaKarte.Input1.service"));
|
||||
});
|
||||
|
||||
m_menu->addSeparator();
|
||||
|
||||
m_showHideAction = m_menu->addAction(i18n("Hide"));
|
||||
connect(m_showHideAction, &QAction::triggered, this, [this]() {
|
||||
toggleWindow();
|
||||
});
|
||||
|
||||
m_quitAction = m_menu->addAction(i18n("Quit"));
|
||||
connect(m_quitAction, &QAction::triggered, this, [this]() {
|
||||
m_quitting = true;
|
||||
QApplication::quit();
|
||||
});
|
||||
|
||||
updateShowHideAction();
|
||||
}
|
||||
|
||||
void TrayController::updateShowHideAction()
|
||||
{
|
||||
if (!m_showHideAction || !m_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_showHideAction->setText(m_window->isVisible() ? i18n("Hide") : i18n("Show"));
|
||||
}
|
||||
|
||||
void TrayController::updateConsoleModeAction()
|
||||
{
|
||||
if (!m_consoleModeAction || !m_config) {
|
||||
return;
|
||||
}
|
||||
m_consoleModeAction->setChecked(m_config->consoleBehaviors());
|
||||
}
|
||||
|
||||
void TrayController::updateMirrorNotifAction()
|
||||
{
|
||||
if (!m_mirrorNotifAction || !m_config) {
|
||||
return;
|
||||
}
|
||||
m_mirrorNotifAction->setChecked(m_config->mirrorNotifications());
|
||||
}
|
||||
|
||||
void TrayController::checkDaemonHealth()
|
||||
{
|
||||
pingDaemon(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
m_gameCenterStatusAction,
|
||||
QStringLiteral("GameCenter"));
|
||||
|
||||
pingDaemon(QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||
QStringLiteral("/org/kde/ALaKarte/Runner1"),
|
||||
QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||
m_runnerStatusAction,
|
||||
QStringLiteral("Runner"));
|
||||
|
||||
pingDaemon(QStringLiteral("org.kde.ALaKarte.Input1"),
|
||||
QStringLiteral("/org/kde/ALaKarte/Input1"),
|
||||
QStringLiteral("org.kde.ALaKarte.Input1"),
|
||||
m_inputStatusAction,
|
||||
QStringLiteral("Input"));
|
||||
}
|
||||
|
||||
void TrayController::pingDaemon(const QString &service, const QString &path, const QString &iface, QAction *statusAction, const QString &daemonName)
|
||||
{
|
||||
if (!statusAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *ifc = new QDBusInterface(service, path, iface, QDBusConnection::sessionBus(), this);
|
||||
auto call = ifc->asyncCall(QStringLiteral("Ping"));
|
||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, statusAction, daemonName, ifc](QDBusPendingCallWatcher *w) {
|
||||
QDBusPendingReply<QString> reply = *w;
|
||||
const bool ok = !reply.isError();
|
||||
updateDaemonStatusAction(statusAction, ok, daemonName);
|
||||
watcher->deleteLater();
|
||||
ifc->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void TrayController::updateDaemonStatusAction(QAction *action, bool ok, const QString &name)
|
||||
{
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
if (ok) {
|
||||
action->setText(i18nc("daemon name, status", "%1: Running", name));
|
||||
} else {
|
||||
action->setText(i18nc("daemon name, status", "%1: Offline", name));
|
||||
}
|
||||
}
|
||||
|
||||
void TrayController::restartDaemon(const QString &unitName)
|
||||
{
|
||||
QProcess::startDetached(QStringLiteral("systemctl"), {QStringLiteral("--user"), QStringLiteral("restart"), unitName});
|
||||
}
|
||||
|
||||
void TrayController::toggleWindow()
|
||||
{
|
||||
if (!m_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_window->isVisible()) {
|
||||
hideWindow();
|
||||
} else {
|
||||
showWindow();
|
||||
}
|
||||
}
|
||||
|
||||
void TrayController::showWindow()
|
||||
{
|
||||
if (!m_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_window->show();
|
||||
m_window->requestActivate();
|
||||
updateShowHideAction();
|
||||
}
|
||||
|
||||
void TrayController::hideWindow()
|
||||
{
|
||||
if (!m_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_window->hide();
|
||||
updateShowHideAction();
|
||||
}
|
||||
57
src/traycontroller.h
Normal file
57
src/traycontroller.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
class QAction;
|
||||
class QEvent;
|
||||
class QMenu;
|
||||
class QTimer;
|
||||
class QWindow;
|
||||
|
||||
class Config;
|
||||
class KStatusNotifierItem;
|
||||
|
||||
class TrayController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TrayController(QWindow *window, bool closeToTray, Config *config, QObject *parent = nullptr);
|
||||
|
||||
~TrayController() override;
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
void updateShowHideAction();
|
||||
void toggleWindow();
|
||||
void showWindow();
|
||||
void hideWindow();
|
||||
|
||||
void rebuildMenu();
|
||||
void updateConsoleModeAction();
|
||||
void updateMirrorNotifAction();
|
||||
void checkDaemonHealth();
|
||||
void updateDaemonStatusAction(QAction *action, bool ok, const QString &name);
|
||||
void restartDaemon(const QString &unitName);
|
||||
void pingDaemon(const QString &service, const QString &path, const QString &iface, QAction *statusAction, const QString &daemonName);
|
||||
|
||||
QPointer<QWindow> m_window;
|
||||
Config *m_config = nullptr;
|
||||
|
||||
KStatusNotifierItem *m_sni = nullptr;
|
||||
QMenu *m_menu = nullptr;
|
||||
QAction *m_showHideAction = nullptr;
|
||||
QAction *m_consoleModeAction = nullptr;
|
||||
QAction *m_mirrorNotifAction = nullptr;
|
||||
QAction *m_gameCenterStatusAction = nullptr;
|
||||
QAction *m_runnerStatusAction = nullptr;
|
||||
QAction *m_inputStatusAction = nullptr;
|
||||
QAction *m_quitAction = nullptr;
|
||||
|
||||
QTimer *m_healthTimer = nullptr;
|
||||
|
||||
bool m_quitting = false;
|
||||
bool m_closeToTray = true;
|
||||
};
|
||||
Loading…
Reference in a new issue