From 0ce8d6eb4640a1ba5590dc53e7e33eae35909c15 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sat, 25 Apr 2026 09:48:42 +0200 Subject: [PATCH] Add HUD toasts and quick-resume shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connect to gameLaunched and gameLaunchFailed signals. Launch toasts dismiss after 3 s; error toasts after 5 s with a red background. The window grows downward from the HUD pill to accommodate the toast pill. When recentGamesChanged fires, the most recently played game is stored in quickResumeGame. While set, the HUD pill widens leftward to show the game name and a play button that calls launchByStorageId directly — no need to open Game Center. --- .../folio/qml/gaming/GamingHUD.qml | 158 +++++++++++++++++- 1 file changed, 150 insertions(+), 8 deletions(-) diff --git a/containments/homescreens/folio/qml/gaming/GamingHUD.qml b/containments/homescreens/folio/qml/gaming/GamingHUD.qml index b39fae4a..e67b0342 100644 --- a/containments/homescreens/folio/qml/gaming/GamingHUD.qml +++ b/containments/homescreens/folio/qml/gaming/GamingHUD.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Controls as QQC2 +import QtQuick.Layouts import QtQuick.Window import org.kde.kirigami as Kirigami @@ -18,11 +19,32 @@ Window { // Guard against startup timing where Kirigami units may briefly be 0/NaN. // LayerShell surfaces must never be committed with zero size. readonly property real safeGridUnit: ((Kirigami.Units.gridUnit || 0) > 0) ? Kirigami.Units.gridUnit : 16 - width: safeGridUnit * 4 - height: safeGridUnit * 2 + + property string toastMessage: "" + property bool toastError: false + readonly property bool toastActive: toastMessage.length > 0 + + // Most-recently-played game for quick resume. Populated from recentGames(1) + // and refreshed whenever the recent list changes. + property var quickResumeGame: null + readonly property bool hasQuickResume: quickResumeGame !== null + + // Window grows leftward from top-right anchor: + // toast active → widest (needs room for message text) + // quick resume → medium (game name + controls) + // idle → compact (controls only) + width: toastActive ? safeGridUnit * 16 : (hasQuickResume ? safeGridUnit * 14 : safeGridUnit * 4) + height: toastActive ? safeGridUnit * 4 : safeGridUnit * 2 color: "transparent" flags: Qt.FramelessWindowHint + Behavior on width { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + Behavior on height { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + LayerShell.Window.scope: "gaming-hud" LayerShell.Window.layer: LayerShell.Window.LayerOverlay LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorRight @@ -38,16 +60,93 @@ Window { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } } + Timer { + id: toastTimer + onTriggered: root.toastMessage = "" + } + + Connections { + target: GamingShell.GameLauncherProvider + function onGameLaunched(name) { + root.toastMessage = i18n("Launching %1", name) + root.toastError = false + toastTimer.interval = 3000 + toastTimer.restart() + } + function onGameLaunchFailed(name, error) { + root.toastMessage = error.length > 0 ? error : i18n("Failed to launch %1", name) + root.toastError = true + toastTimer.interval = 5000 + toastTimer.restart() + } + function onRecentGamesChanged() { + const recent = GamingShell.GameLauncherProvider.recentGames(1) + root.quickResumeGame = recent.length > 0 ? recent[0] : null + } + } + + Component.onCompleted: { + const recent = GamingShell.GameLauncherProvider.recentGames(1) + root.quickResumeGame = recent.length > 0 ? recent[0] : null + } + + // ---- HUD pill (always visible, fills window width, grows leftward) ---- Rectangle { - anchors.fill: parent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right anchors.margins: Kirigami.Units.smallSpacing + height: safeGridUnit * 2 - Kirigami.Units.smallSpacing * 2 radius: height / 2 color: Qt.rgba(0, 0, 0, 0.55) - Row { - anchors.centerIn: parent - spacing: Kirigami.Units.smallSpacing + RowLayout { + anchors { + fill: parent + leftMargin: Kirigami.Units.smallSpacing + rightMargin: Kirigami.Units.smallSpacing + } + spacing: 0 + // Quick-resume section — only visible when a recent game exists + QQC2.ToolButton { + visible: root.hasQuickResume + icon.name: "media-playback-start" + icon.color: "white" + display: QQC2.AbstractButton.IconOnly + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: root.quickResumeGame ? i18n("Resume %1", root.quickResumeGame.name) : "" + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + onClicked: { + if (root.quickResumeGame) { + GamingShell.GameLauncherProvider.launchByStorageId(root.quickResumeGame.storageId) + } + } + } + + QQC2.Label { + visible: root.hasQuickResume + Layout.fillWidth: true + text: root.quickResumeGame ? root.quickResumeGame.name : "" + color: "white" + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 + elide: Text.ElideRight + maximumLineCount: 1 + leftPadding: Kirigami.Units.smallSpacing + } + + // Separator between quick-resume and controls + Rectangle { + visible: root.hasQuickResume + width: 1 + implicitHeight: Kirigami.Units.gridUnit + color: Qt.rgba(1, 1, 1, 0.25) + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Kirigami.Units.smallSpacing + Layout.rightMargin: Kirigami.Units.smallSpacing + } + + // Game Center button QQC2.ToolButton { icon.name: "input-gaming" icon.color: "white" @@ -57,7 +156,7 @@ Window { onClicked: root.openRequested() } - // Show primary gamepad battery when connected + // Primary gamepad battery QQC2.Label { visible: GamingShell.GamepadManager.hasGamepad && GamingShell.GamepadManager.primaryGamepad @@ -67,7 +166,50 @@ Window { : "" color: "white" font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 - anchors.verticalCenter: parent.verticalCenter + rightPadding: Kirigami.Units.smallSpacing + } + } + } + + // ---- toast pill (slides in below HUD pill when active) ---- + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Kirigami.Units.smallSpacing + height: safeGridUnit * 2 - Kirigami.Units.smallSpacing * 2 + radius: height / 2 + color: root.toastError ? Qt.rgba(0.75, 0.1, 0.05, 0.9) : Qt.rgba(0, 0, 0, 0.55) + + opacity: root.toastActive ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad } + } + + RowLayout { + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + leftMargin: Kirigami.Units.largeSpacing + rightMargin: Kirigami.Units.largeSpacing + } + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + source: root.toastError ? "dialog-warning" : "media-playback-start" + implicitWidth: Kirigami.Units.iconSizes.small + implicitHeight: Kirigami.Units.iconSizes.small + Layout.alignment: Qt.AlignVCenter + } + + QQC2.Label { + Layout.fillWidth: true + text: root.toastMessage + color: "white" + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 + elide: Text.ElideRight + maximumLineCount: 1 } } }