noctalia-shell/Widgets/Sidebar/Panel/BluetoothPanel.qml
2025-07-11 14:14:28 +02:00

343 lines
14 KiB
QML

import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Quickshell.Wayland
import Quickshell
import Quickshell.Bluetooth
import qs.Settings
import qs.Components
import qs.Helpers
Item {
id: root
property alias panel: bluetoothPanelModal
// For showing error/status messages
property string statusMessage: ""
property bool statusPopupVisible: false
function showStatus(msg) {
statusMessage = msg
statusPopupVisible = true
}
function hideStatus() {
statusPopupVisible = false
}
function showAt() {
bluetoothLogic.showAt()
}
Rectangle {
id: card
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
Text {
anchors.centerIn: parent
text: "bluetooth"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: bluetoothButtonArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
}
MouseArea {
id: bluetoothButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: bluetoothLogic.showAt()
}
}
QtObject {
id: bluetoothLogic
function showAt() {
if (Bluetooth.defaultAdapter) {
if (!Bluetooth.defaultAdapter.enabled)
Bluetooth.defaultAdapter.enabled = true
if (!Bluetooth.defaultAdapter.discovering)
Bluetooth.defaultAdapter.discovering = true
}
bluetoothPanelModal.visible = true
}
}
PanelWindow {
id: bluetoothPanelModal
implicitWidth: 480
implicitHeight: 720
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: -24
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
onVisibleChanged: {
if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering)
Bluetooth.defaultAdapter.discovering = false
}
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 24
ColumnLayout {
anchors.fill: parent
anchors.margins: 32
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: 20
Layout.preferredHeight: 48
Layout.leftMargin: 16
Layout.rightMargin: 16
Text {
text: "bluetooth"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.accentPrimary
}
Text {
text: "Bluetooth"
font.pixelSize: 26
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
Rectangle {
width: 36; height: 36; radius: 18
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "close"
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 20
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: bluetoothPanelModal.visible = false
cursorShape: Qt.PointingHandCursor
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.outline
opacity: 0.12
}
// Content area (centered, in a card)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 520
Layout.alignment: Qt.AlignHCenter
Layout.margins: 0
color: Theme.surfaceVariant
radius: 18
border.color: Theme.outline
border.width: 1
anchors.topMargin: 32
Rectangle {
id: bg
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 12
border.width: 1
border.color: Theme.surfaceVariant
z: 0
}
Rectangle {
id: header
color: "transparent"
}
Rectangle {
id: listContainer
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 24
color: "transparent"
clip: true
ListView {
id: deviceListView
anchors.fill: parent
spacing: 4
boundsBehavior: Flickable.StopAtBounds
model: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : []
delegate: Rectangle {
width: parent.width
height: 60
color: "transparent"
radius: 8
property bool userInitiatedDisconnect: false
Rectangle {
anchors.fill: parent
radius: 8
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18)
: (deviceMouseArea.containsMouse ? Theme.highlight : "transparent")
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
// Fixed-width icon for alignment
Text {
width: 28
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: modelData.connected ? "bluetooth" : "bluetooth_disabled"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
// Device name always fills width for alignment
Text {
Layout.fillWidth: true
text: modelData.name || "Unknown Device"
color: modelData.connected ? Theme.accentPrimary : Theme.textPrimary
font.pixelSize: 14
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: modelData.address
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
font.pixelSize: 11
elide: Text.ElideRight
}
Text {
text: "Paired: " + modelData.paired + " | Trusted: " + modelData.trusted
font.pixelSize: 10
color: Theme.textSecondary
visible: true
}
// No "Connected" text here!
}
Spinner {
running: modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting
color: Theme.textPrimary
size: 16
visible: running
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected) {
userInitiatedDisconnect = true
modelData.disconnect()
} else if (!modelData.paired) {
modelData.pair()
root.showStatus("Pairing... Please check your phone or system for a PIN dialog.")
} else {
modelData.connect()
}
}
}
Connections {
target: modelData
function onPairedChanged() {
if (modelData.paired) {
root.showStatus("Paired! Now connecting...")
modelData.connect()
}
}
function onPairingChanged() {
if (!modelData.pairing && !modelData.paired) {
root.showStatus("Pairing failed or was cancelled.")
}
}
function onConnectedChanged() {
userInitiatedDisconnect = false
}
function onStateChanged() {
// Optionally handle more granular feedback here
}
}
}
}
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: listContainer.top
anchors.bottom: listContainer.bottom
width: 4
radius: 2
color: Theme.textSecondary
opacity: deviceListView.contentHeight > deviceListView.height ? 0.3 : 0
visible: opacity > 0
}
}
}
}
// Status/Info popup
Popup {
id: statusPopup
x: (parent.width - width) / 2
y: 40
width: Math.min(360, parent.width - 40)
visible: root.statusPopupVisible
modal: false
focus: false
background: Rectangle {
color: Theme.accentPrimary // Use your theme's accent color
radius: 8
}
contentItem: Text {
text: root.statusMessage
color: "white"
wrapMode: Text.WordWrap
padding: 12
font.pixelSize: 14
}
onVisibleChanged: {
if (visible) {
// Auto-hide after 3 seconds
statusPopupTimer.restart()
}
}
}
}
}