mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-12 17:27:43 +00:00
Slide-out panel with brightness, volume, Wi-Fi, Bluetooth, and airplane mode. Fully gamepad-navigable; D-pad and stick input routed to the panel while open. System status bar (clock, battery, connectivity) added to game center header. Lutris games discovered from ~/.local/share/lutris/pga.db, Heroic from ~/.config/heroic/store_cache/ JSON files. Deduplication extended to cover all launcher sources. Desktop entries launched via KIO::ApplicationLauncherJob to expand Exec field codes. Generic commands parsed with KShell::splitArgs. SQLite uses RAII cleanup guard. Clamped SDL axis, rumble, and LED values. Renamed rumble params from frequency to intensity. GamepadManager uses singleton factory. setPlayerIndex checks SDL return. FavouritesBar popup uses screen virtualX/Y for multi-monitor.
412 lines
14 KiB
QML
412 lines
14 KiB
QML
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls as QQC2
|
|
|
|
import org.kde.kirigami as Kirigami
|
|
import org.kde.plasma.components 3.0 as PC3
|
|
import org.kde.plasma.private.mobileshell as MobileShell
|
|
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
|
|
import org.kde.plasma.private.mobileshell.screenbrightnessplugin as ScreenBrightness
|
|
import org.kde.plasma.private.volume
|
|
import org.kde.plasma.networkmanagement as PlasmaNM
|
|
import org.kde.bluezqt 1.0 as BluezQt
|
|
|
|
Item {
|
|
id: root
|
|
anchors.fill: parent
|
|
|
|
property bool opened: false
|
|
|
|
// Focusable controls for gamepad navigation
|
|
property var _controls: []
|
|
property int _focusIndex: 0
|
|
|
|
function _buildControlsList() {
|
|
var list = []
|
|
if (screenBrightness.brightnessAvailable) list.push(brightnessSlider)
|
|
if (PreferredDevice.sink) list.push(volumeSlider)
|
|
list.push(wifiSwitch)
|
|
list.push(btSwitch)
|
|
list.push(airplaneSwitch)
|
|
_controls = list
|
|
}
|
|
|
|
function open() {
|
|
opened = true
|
|
_buildControlsList()
|
|
_focusIndex = 0
|
|
_highlightCurrent()
|
|
}
|
|
function close() {
|
|
opened = false
|
|
}
|
|
function toggle() {
|
|
if (opened) close(); else open()
|
|
}
|
|
|
|
function _highlightCurrent() {
|
|
if (_controls.length > 0 && _focusIndex >= 0 && _focusIndex < _controls.length) {
|
|
_controls[_focusIndex].forceActiveFocus()
|
|
}
|
|
}
|
|
|
|
// Gamepad input handlers called from GameCenterOverlay
|
|
function gamepadUp() {
|
|
if (_focusIndex > 0) {
|
|
_focusIndex--
|
|
_highlightCurrent()
|
|
}
|
|
}
|
|
function gamepadDown() {
|
|
if (_focusIndex < _controls.length - 1) {
|
|
_focusIndex++
|
|
_highlightCurrent()
|
|
}
|
|
}
|
|
function gamepadLeft() {
|
|
var ctrl = _controls[_focusIndex]
|
|
if (ctrl instanceof PC3.Slider) {
|
|
ctrl.decrease()
|
|
ctrl.moved()
|
|
}
|
|
}
|
|
function gamepadRight() {
|
|
var ctrl = _controls[_focusIndex]
|
|
if (ctrl instanceof PC3.Slider) {
|
|
ctrl.increase()
|
|
ctrl.moved()
|
|
}
|
|
}
|
|
function gamepadAccept() {
|
|
var ctrl = _controls[_focusIndex]
|
|
if (ctrl instanceof QQC2.Switch) {
|
|
ctrl.toggle()
|
|
ctrl.toggled()
|
|
}
|
|
}
|
|
|
|
// Eat clicks on the dimmed backdrop
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
visible: root.opened
|
|
onClicked: root.close()
|
|
}
|
|
|
|
// Dim backdrop
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: "black"
|
|
opacity: root.opened ? 0.4 : 0
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
|
}
|
|
}
|
|
|
|
// Panel sliding in from the right
|
|
Rectangle {
|
|
id: panel
|
|
width: Math.min(root.width * 0.35, Kirigami.Units.gridUnit * 22)
|
|
height: root.height
|
|
anchors.top: root.top
|
|
anchors.bottom: root.bottom
|
|
|
|
x: root.opened ? root.width - width : root.width
|
|
|
|
Behavior on x {
|
|
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
|
}
|
|
|
|
Kirigami.Theme.inherit: false
|
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
|
color: Qt.rgba(Kirigami.Theme.backgroundColor.r,
|
|
Kirigami.Theme.backgroundColor.g,
|
|
Kirigami.Theme.backgroundColor.b, 0.96)
|
|
|
|
// Subtle left border
|
|
Rectangle {
|
|
width: 1
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
color: Kirigami.Theme.disabledTextColor
|
|
opacity: 0.3
|
|
}
|
|
|
|
ScreenBrightness.ScreenBrightnessUtil {
|
|
id: screenBrightness
|
|
}
|
|
|
|
PlasmaNM.Handler {
|
|
id: nmHandler
|
|
}
|
|
|
|
PlasmaNM.EnabledConnections {
|
|
id: enabledConnections
|
|
}
|
|
|
|
Flickable {
|
|
anchors.fill: parent
|
|
anchors.margins: Kirigami.Units.largeSpacing * 2
|
|
contentHeight: settingsColumn.implicitHeight
|
|
clip: true
|
|
|
|
ColumnLayout {
|
|
id: settingsColumn
|
|
width: parent.width
|
|
spacing: Kirigami.Units.largeSpacing * 2
|
|
|
|
// ---- Header ----
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
|
|
Kirigami.Heading {
|
|
text: i18n("Quick Settings")
|
|
level: 2
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
QQC2.ToolButton {
|
|
icon.name: "window-close-symbolic"
|
|
onClicked: root.close()
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator { Layout.fillWidth: true }
|
|
|
|
// ---- Brightness ----
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
visible: screenBrightness.brightnessAvailable
|
|
|
|
PC3.Label {
|
|
text: i18n("Brightness")
|
|
font.bold: true
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
Kirigami.Icon {
|
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
source: "low-brightness"
|
|
}
|
|
|
|
PC3.Slider {
|
|
id: brightnessSlider
|
|
Layout.fillWidth: true
|
|
from: 1
|
|
to: screenBrightness.maxBrightness
|
|
stepSize: Math.max(1, Math.round(screenBrightness.maxBrightness / 20))
|
|
value: screenBrightness.brightness
|
|
onMoved: screenBrightness.brightness = value
|
|
|
|
Timer {
|
|
interval: 0
|
|
running: true
|
|
repeat: false
|
|
onTriggered: brightnessSlider.value = Qt.binding(() => screenBrightness.brightness)
|
|
}
|
|
|
|
// Focus highlight
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: -Kirigami.Units.smallSpacing
|
|
radius: Kirigami.Units.smallSpacing
|
|
color: "transparent"
|
|
border.color: Kirigami.Theme.highlightColor
|
|
border.width: parent.activeFocus ? 2 : 0
|
|
}
|
|
}
|
|
|
|
Kirigami.Icon {
|
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
source: "high-brightness"
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Volume ----
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
visible: PreferredDevice.sink != null
|
|
|
|
PC3.Label {
|
|
text: i18n("Volume")
|
|
font.bold: true
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
Kirigami.Icon {
|
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
source: "audio-volume-low"
|
|
}
|
|
|
|
PC3.Slider {
|
|
id: volumeSlider
|
|
Layout.fillWidth: true
|
|
from: PulseAudio.MinimalVolume
|
|
to: PulseAudio.NormalVolume
|
|
stepSize: PulseAudio.NormalVolume / 20
|
|
value: PreferredDevice.sink ? PreferredDevice.sink.volume : 0
|
|
onMoved: {
|
|
if (PreferredDevice.sink) {
|
|
PreferredDevice.sink.volume = value
|
|
PreferredDevice.sink.muted = (value === 0)
|
|
}
|
|
}
|
|
|
|
// Focus highlight
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: -Kirigami.Units.smallSpacing
|
|
radius: Kirigami.Units.smallSpacing
|
|
color: "transparent"
|
|
border.color: Kirigami.Theme.highlightColor
|
|
border.width: parent.activeFocus ? 2 : 0
|
|
}
|
|
}
|
|
|
|
Kirigami.Icon {
|
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
source: "audio-volume-high"
|
|
}
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator { Layout.fillWidth: true }
|
|
|
|
// ---- Connectivity toggles ----
|
|
PC3.Label {
|
|
text: i18n("Connectivity")
|
|
font.bold: true
|
|
}
|
|
|
|
GridLayout {
|
|
Layout.fillWidth: true
|
|
columns: 2
|
|
rowSpacing: Kirigami.Units.smallSpacing
|
|
columnSpacing: Kirigami.Units.largeSpacing
|
|
|
|
// WiFi toggle
|
|
QQC2.Switch {
|
|
id: wifiSwitch
|
|
text: i18n("Wi-Fi")
|
|
checked: enabledConnections.wirelessEnabled
|
|
onToggled: nmHandler.enableWireless(checked)
|
|
}
|
|
|
|
// Bluetooth toggle
|
|
QQC2.Switch {
|
|
id: btSwitch
|
|
text: i18n("Bluetooth")
|
|
checked: !BluezQt.Manager.bluetoothBlocked
|
|
onToggled: BluezQt.Manager.bluetoothBlocked = !checked
|
|
}
|
|
|
|
// Airplane mode
|
|
QQC2.Switch {
|
|
id: airplaneSwitch
|
|
text: i18n("Airplane Mode")
|
|
checked: PlasmaNM.Configuration.airplaneModeEnabled
|
|
onToggled: {
|
|
nmHandler.enableAirplaneMode(!PlasmaNM.Configuration.airplaneModeEnabled)
|
|
PlasmaNM.Configuration.airplaneModeEnabled = !PlasmaNM.Configuration.airplaneModeEnabled
|
|
}
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator { Layout.fillWidth: true }
|
|
|
|
// ---- Battery info ----
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
visible: MobileShell.BatteryInfo.isVisible
|
|
|
|
PC3.Label {
|
|
text: i18n("Battery")
|
|
font.bold: true
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
MobileShell.BatteryIndicator {
|
|
textPixelSize: Kirigami.Units.gridUnit * 0.7
|
|
}
|
|
|
|
PC3.Label {
|
|
text: MobileShell.BatteryInfo.pluggedIn ? i18n("Charging") : ""
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
|
opacity: 0.7
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Controller info ----
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
visible: GamingShell.GamepadManager.hasGamepad
|
|
|
|
PC3.Label {
|
|
text: i18n("Controllers")
|
|
font.bold: true
|
|
}
|
|
|
|
Repeater {
|
|
model: GamingShell.GamepadManager
|
|
|
|
RowLayout {
|
|
spacing: Kirigami.Units.smallSpacing
|
|
required property string name
|
|
required property int battery
|
|
required property string type
|
|
|
|
Kirigami.Icon {
|
|
implicitWidth: Kirigami.Units.iconSizes.small
|
|
implicitHeight: Kirigami.Units.iconSizes.small
|
|
source: "input-gaming"
|
|
}
|
|
|
|
PC3.Label {
|
|
text: name
|
|
}
|
|
|
|
PC3.Label {
|
|
text: battery >= 0 ? battery + "%" : i18n("Wired")
|
|
opacity: 0.7
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator { Layout.fillWidth: true }
|
|
|
|
// ---- Gamepad legend ----
|
|
PC3.Label {
|
|
Layout.fillWidth: true
|
|
text: i18n("↕: Navigate ↔: Adjust A: Toggle B: Close")
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
|
|
opacity: 0.5
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Item { Layout.fillHeight: true }
|
|
}
|
|
}
|
|
}
|
|
}
|