bluetooth menu

This commit is contained in:
end-4
2025-08-30 07:51:18 +02:00
parent 59abffb1c1
commit a952ea02dc
10 changed files with 276 additions and 39 deletions
@@ -0,0 +1,23 @@
pragma Singleton
// From https://github.com/caelestia-dots/shell (GPLv3)
import Quickshell
Singleton {
id: root
function getBluetoothDeviceMaterialSymbol(systemIconName: string): string {
if (systemIconName.includes("headset") || systemIconName.includes("headphones"))
return "headphones";
if (systemIconName.includes("audio"))
return "speaker";
if (systemIconName.includes("phone"))
return "smartphone";
if (systemIconName.includes("mouse"))
return "mouse";
if (systemIconName.includes("keyboard"))
return "keyboard";
return "bluetooth";
}
}
@@ -20,6 +20,7 @@ RippleButton {
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: Appearance.colors.colLayer3Hover colBackgroundHover: Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active colRipple: Appearance.colors.colLayer3Active
property alias colText: buttonTextWidget.color
contentItem: StyledText { contentItem: StyledText {
id: buttonTextWidget id: buttonTextWidget
@@ -0,0 +1,25 @@
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: root
property bool active: false
horizontalPadding: Appearance.rounding.large
verticalPadding: 12
clip: true
pointingHandCursor: !active
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: contentItem.implicitHeight + verticalPadding * 2
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: active ? colBackground : Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
buttonRadius: 0
}
@@ -12,6 +12,7 @@ Button {
id: root id: root
property bool toggled property bool toggled
property string buttonText property string buttonText
property bool pointingHandCursor: true
property real buttonRadius: Appearance?.rounding?.small ?? 4 property real buttonRadius: Appearance?.rounding?.small ?? 4
property real buttonRadiusPressed: buttonRadius property real buttonRadiusPressed: buttonRadius
property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
@@ -58,7 +59,7 @@ Button {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: root.pointingHandCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => { onPressed: (event) => {
if(event.button === Qt.RightButton) { if(event.button === Qt.RightButton) {
@@ -1,18 +1,15 @@
import qs import qs
import qs.services import qs.services
import qs.services.network
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions
import "./quickToggles/" import "./quickToggles/"
import "./wifiNetworks/" import "./wifiNetworks/"
import "./bluetoothDevices/"
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Bluetooth
import Quickshell.Hyprland import Quickshell.Hyprland
Item { Item {
@@ -21,12 +18,14 @@ Item {
property int sidebarPadding: 12 property int sidebarPadding: 12
property string settingsQmlPath: Quickshell.shellPath("settings.qml") property string settingsQmlPath: Quickshell.shellPath("settings.qml")
property bool showWifiDialog: false property bool showWifiDialog: false
property bool showBluetoothDialog: false
Connections { Connections {
target: GlobalStates target: GlobalStates
function onSidebarRightOpenChanged() { function onSidebarRightOpenChanged() {
if (!GlobalStates.sidebarRightOpen) { if (!GlobalStates.sidebarRightOpen) {
root.showWifiDialog = false; root.showWifiDialog = false;
root.showBluetoothDialog = false;
} }
} }
} }
@@ -129,7 +128,13 @@ Item {
root.showWifiDialog = true; root.showWifiDialog = true;
} }
} }
BluetoothToggle {} BluetoothToggle {
altAction: () => {
Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true;
root.showBluetoothDialog = true;
}
}
NightLight {} NightLight {}
GameMode {} GameMode {}
IdleInhibitor {} IdleInhibitor {}
@@ -138,7 +143,6 @@ Item {
} }
CenterWidgetGroup { CenterWidgetGroup {
focus: sidebarRoot.visible
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
@@ -176,4 +180,28 @@ Item {
} }
} }
} }
onShowBluetoothDialogChanged: if (showBluetoothDialog) bluetoothDialogLoader.active = true;
Loader {
id: bluetoothDialogLoader
anchors.fill: parent
active: root.showBluetoothDialog || item.visible
onActiveChanged: {
if (active) {
item.show = true;
item.forceActiveFocus();
}
}
sourceComponent: BluetoothDialog {
onDismiss: {
show = false
root.showBluetoothDialog = false
}
onVisibleChanged: {
if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false;
}
}
}
} }
@@ -0,0 +1,112 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
DialogListItem {
id: root
required property var device
property bool expanded: false
pointingHandCursor: !expanded
onClicked: expanded = !expanded
component ActionButton: DialogButton {
colBackground: Appearance.colors.colPrimary
colBackgroundHover: Appearance.colors.colPrimaryHover
colRipple: Appearance.colors.colPrimaryActive
colText: Appearance.colors.colOnPrimary
}
contentItem: ColumnLayout {
anchors {
fill: parent
topMargin: root.verticalPadding
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
spacing: 0
RowLayout {
// Name
spacing: 10
MaterialSymbol {
iconSize: Appearance.font.pixelSize.larger
text: Icons.getBluetoothDeviceMaterialSymbol(root.device?.icon || "")
color: Appearance.colors.colOnSurfaceVariant
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
color: Appearance.colors.colOnSurfaceVariant
elide: Text.ElideRight
text: root.device?.name
}
StyledText {
visible: root.device?.connected || root.device?.paired
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
elide: Text.ElideRight
text: {
if (!root.device?.paired) return "";
let statusText = root.device?.connected ? Translation.tr("Connected") : Translation.tr("Paired");
if (!root.device?.batteryAvailable) return statusText;
statusText += ` ${root.device?.battery * 100}%`;
return statusText;
}
}
}
MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer3
rotation: root.expanded ? 180 : 0
Behavior on rotation {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
}
RowLayout {
visible: root.expanded
Layout.topMargin: 8
Item {
Layout.fillWidth: true
}
ActionButton {
buttonText: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect")
onClicked: {
if (root.device?.connected) {
root.device.disconnect();
} else {
root.device.connect();
}
}
}
ActionButton {
visible: root.device?.paired
colBackground: Appearance.colors.colError
colBackgroundHover: Appearance.colors.colErrorHover
colRipple: Appearance.colors.colErrorActive
colText: Appearance.colors.colOnError
buttonText: Translation.tr("Forget")
onClicked: {
root.device?.forget();
}
}
}
Item {
Layout.fillHeight: true
}
}
}
@@ -0,0 +1,67 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell.Bluetooth
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog {
id: root
WindowDialogTitle {
text: Translation.tr("Bluetooth devices")
}
// TODO: add indeterminate progress bar when scanning
WindowDialogSeparator {}
StyledListView {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: -15
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
clip: true
spacing: 0
animateAppearance: false
model: ScriptModel {
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired))
}
delegate: BluetoothDeviceItem {
required property BluetoothDevice modelData
device: modelData
anchors {
left: parent?.left
right: parent?.right
}
}
}
WindowDialogSeparator {}
WindowDialogButtonRow {
DialogButton {
buttonText: Translation.tr("Details")
onClicked: {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]);
GlobalStates.sidebarRightOpen = false;
}
}
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}
@@ -3,17 +3,9 @@ import qs.services
import qs.services.network import qs.services.network
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions
import "./quickToggles/"
import "./wifiNetworks/"
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog { WindowDialog {
id: root id: root
@@ -44,7 +36,6 @@ WindowDialog {
return b.strength - a.strength; return b.strength - a.strength;
}) })
} }
// model: Network.wifiNetworks
delegate: WifiNetworkItem { delegate: WifiNetworkItem {
required property WifiAccessPoint modelData required property WifiAccessPoint modelData
wifiNetwork: modelData wifiNetwork: modelData
@@ -1,37 +1,21 @@
import qs import qs
import qs.modules.common import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.services import qs.services
import qs.services.network import qs.services.network
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
RippleButton { DialogListItem {
id: root id: root
required property WifiAccessPoint wifiNetwork required property WifiAccessPoint wifiNetwork
horizontalPadding: Appearance.rounding.large active: (wifiNetwork?.askingPassword || wifiNetwork?.active)
verticalPadding: 12
implicitWidth: mainLayout.implicitWidth + horizontalPadding * 2
implicitHeight: mainLayout.implicitHeight + verticalPadding * 2
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
clip: true
buttonRadius: 0
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: (wifiNetwork?.askingPassword || wifiNetwork?.active) ? colBackground : Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
onClicked: { onClicked: {
Network.connectToWifiNetwork(wifiNetwork); Network.connectToWifiNetwork(wifiNetwork);
} }
contentItem: ColumnLayout { contentItem: ColumnLayout {
id: mainLayout
anchors { anchors {
fill: parent fill: parent
topMargin: root.verticalPadding topMargin: root.verticalPadding
@@ -52,8 +36,9 @@ RippleButton {
} }
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown")
color: Appearance.colors.colOnSurfaceVariant color: Appearance.colors.colOnSurfaceVariant
elide: Text.ElideRight
text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown")
} }
MaterialSymbol { MaterialSymbol {
visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false
@@ -65,8 +50,8 @@ RippleButton {
ColumnLayout { // Password ColumnLayout { // Password
id: passwordPrompt id: passwordPrompt
visible: root.wifiNetwork?.askingPassword ?? false
Layout.topMargin: 8 Layout.topMargin: 8
visible: root.wifiNetwork?.askingPassword ?? false
MaterialTextField { MaterialTextField {
id: passwordField id: passwordField
@@ -107,8 +92,8 @@ RippleButton {
ColumnLayout { // Public wifi login page ColumnLayout { // Public wifi login page
id: publicWifiPortal id: publicWifiPortal
visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0
Layout.topMargin: 8 Layout.topMargin: 8
visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0
RowLayout { RowLayout {
DialogButton { DialogButton {
@@ -124,5 +109,9 @@ RippleButton {
} }
} }
} }
Item {
Layout.fillHeight: true
}
} }
} }
@@ -12,7 +12,7 @@ import QtQuick
Singleton { Singleton {
id: root id: root
readonly property bool enabled: Bluetooth.defaultAdapter?.enabled readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false
readonly property BluetoothDevice firstActiveDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null readonly property BluetoothDevice firstActiveDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null
readonly property int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0 readonly property int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0
readonly property bool connected: Bluetooth.devices.values.some(d => d.connected) readonly property bool connected: Bluetooth.devices.values.some(d => d.connected)