mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 00:47:22 +00:00
Use Motion tokens for the Game Center overlay, gaming HUD, quick settings drawer, and running-games panel. Keep gaming mode settings and gamepad behavior unchanged.
220 lines
8.5 KiB
QML
220 lines
8.5 KiB
QML
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls as QQC2
|
|
import QtQuick.Layouts
|
|
import QtQuick.Window
|
|
|
|
import org.kde.kirigami as Kirigami
|
|
import org.kde.plasma.private.mobileshell as MobileShell
|
|
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
|
import org.kde.layershell 1.0 as LayerShell
|
|
|
|
Window {
|
|
id: root
|
|
|
|
signal openRequested()
|
|
|
|
// 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
|
|
|
|
property string toastMessage: ""
|
|
property bool toastError: false
|
|
readonly property bool toastActive: toastMessage.length > 0
|
|
readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
|
|
readonly property int longAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsDefault)
|
|
|
|
// 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 {
|
|
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
|
|
}
|
|
Behavior on height {
|
|
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
|
|
}
|
|
|
|
LayerShell.Window.scope: "gaming-hud"
|
|
LayerShell.Window.layer: LayerShell.Window.LayerOverlay
|
|
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorRight
|
|
LayerShell.Window.exclusionZone: 0
|
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
|
|
|
// Driven by the Loader in folio/qml/main.qml — set false to fade out
|
|
// before the Loader destroys the window.
|
|
property bool showing: true
|
|
|
|
opacity: showing ? 1 : 0
|
|
Behavior on opacity {
|
|
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsDefault; duration: root.longAnimationDuration }
|
|
}
|
|
|
|
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.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)
|
|
|
|
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"
|
|
display: QQC2.AbstractButton.IconOnly
|
|
QQC2.ToolTip.visible: hovered
|
|
QQC2.ToolTip.text: i18n("Game Center")
|
|
onClicked: root.openRequested()
|
|
}
|
|
|
|
// Primary gamepad battery
|
|
QQC2.Label {
|
|
visible: GamingShell.GamepadManager.hasGamepad
|
|
&& GamingShell.GamepadManager.primaryGamepad
|
|
&& GamingShell.GamepadManager.primaryGamepad.batteryPercent >= 0
|
|
text: GamingShell.GamepadManager.primaryGamepad
|
|
? GamingShell.GamepadManager.primaryGamepad.batteryPercent + "%"
|
|
: ""
|
|
color: "white"
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
|
|
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 {
|
|
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
|
|
}
|
|
|
|
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
|
|
isMask: true
|
|
color: "white"
|
|
}
|
|
|
|
QQC2.Label {
|
|
Layout.fillWidth: true
|
|
text: root.toastMessage
|
|
color: "white"
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 1
|
|
}
|
|
}
|
|
}
|
|
}
|