diff --git a/.config/quickshell/ii/modules/common/Icons.qml b/.config/quickshell/ii/modules/common/Icons.qml new file mode 100644 index 000000000..454aea11e --- /dev/null +++ b/.config/quickshell/ii/modules/common/Icons.qml @@ -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"; + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml index ba00d9b3c..9373a8cd5 100644 --- a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml @@ -20,6 +20,7 @@ RippleButton { colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) colBackgroundHover: Appearance.colors.colLayer3Hover colRipple: Appearance.colors.colLayer3Active + property alias colText: buttonTextWidget.color contentItem: StyledText { id: buttonTextWidget diff --git a/.config/quickshell/ii/modules/common/widgets/DialogListItem.qml b/.config/quickshell/ii/modules/common/widgets/DialogListItem.qml new file mode 100644 index 000000000..67205ced7 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/DialogListItem.qml @@ -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 +} diff --git a/.config/quickshell/ii/modules/common/widgets/RippleButton.qml b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml index b90c5e291..07e6c5318 100644 --- a/.config/quickshell/ii/modules/common/widgets/RippleButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml @@ -12,6 +12,7 @@ Button { id: root property bool toggled property string buttonText + property bool pointingHandCursor: true property real buttonRadius: Appearance?.rounding?.small ?? 4 property real buttonRadiusPressed: buttonRadius property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius @@ -58,7 +59,7 @@ Button { MouseArea { anchors.fill: parent - cursorShape: Qt.PointingHandCursor + cursorShape: root.pointingHandCursor ? Qt.PointingHandCursor : Qt.ArrowCursor acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onPressed: (event) => { if(event.button === Qt.RightButton) { diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index 37878a669..b1adc3ae6 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -1,18 +1,15 @@ import qs import qs.services -import qs.services.network import qs.modules.common import qs.modules.common.widgets -import qs.modules.common.functions import "./quickToggles/" import "./wifiNetworks/" +import "./bluetoothDevices/" import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Io import Quickshell -import Quickshell.Wayland +import Quickshell.Bluetooth import Quickshell.Hyprland Item { @@ -21,12 +18,14 @@ Item { property int sidebarPadding: 12 property string settingsQmlPath: Quickshell.shellPath("settings.qml") property bool showWifiDialog: false + property bool showBluetoothDialog: false Connections { target: GlobalStates function onSidebarRightOpenChanged() { if (!GlobalStates.sidebarRightOpen) { root.showWifiDialog = false; + root.showBluetoothDialog = false; } } } @@ -129,7 +128,13 @@ Item { root.showWifiDialog = true; } } - BluetoothToggle {} + BluetoothToggle { + altAction: () => { + Bluetooth.defaultAdapter.enabled = true; + Bluetooth.defaultAdapter.discovering = true; + root.showBluetoothDialog = true; + } + } NightLight {} GameMode {} IdleInhibitor {} @@ -138,7 +143,6 @@ Item { } CenterWidgetGroup { - focus: sidebarRoot.visible Layout.alignment: Qt.AlignHCenter Layout.fillHeight: 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; + } + } + } } diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml new file mode 100644 index 000000000..19ee07234 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml @@ -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 + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml new file mode 100644 index 000000000..4e5d99921 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml @@ -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() + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml index 6569704a8..abd854cfc 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml @@ -3,17 +3,9 @@ import qs.services import qs.services.network import qs.modules.common import qs.modules.common.widgets -import qs.modules.common.functions -import "./quickToggles/" -import "./wifiNetworks/" import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Io import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland WindowDialog { id: root @@ -44,7 +36,6 @@ WindowDialog { return b.strength - a.strength; }) } - // model: Network.wifiNetworks delegate: WifiNetworkItem { required property WifiAccessPoint modelData wifiNetwork: modelData diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml index 6afeab875..ac178fbe2 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -1,37 +1,21 @@ import qs import qs.modules.common -import qs.modules.common.functions import qs.modules.common.widgets import qs.services import qs.services.network import QtQuick import QtQuick.Layouts -import Quickshell -RippleButton { +DialogListItem { id: root required property WifiAccessPoint wifiNetwork - horizontalPadding: Appearance.rounding.large - 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 - + active: (wifiNetwork?.askingPassword || wifiNetwork?.active) onClicked: { Network.connectToWifiNetwork(wifiNetwork); } contentItem: ColumnLayout { - id: mainLayout anchors { fill: parent topMargin: root.verticalPadding @@ -52,8 +36,9 @@ RippleButton { } StyledText { Layout.fillWidth: true - text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown") color: Appearance.colors.colOnSurfaceVariant + elide: Text.ElideRight + text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown") } MaterialSymbol { visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false @@ -65,8 +50,8 @@ RippleButton { ColumnLayout { // Password id: passwordPrompt - visible: root.wifiNetwork?.askingPassword ?? false Layout.topMargin: 8 + visible: root.wifiNetwork?.askingPassword ?? false MaterialTextField { id: passwordField @@ -107,8 +92,8 @@ RippleButton { ColumnLayout { // Public wifi login page id: publicWifiPortal - visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0 Layout.topMargin: 8 + visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0 RowLayout { DialogButton { @@ -124,5 +109,9 @@ RippleButton { } } } + + Item { + Layout.fillHeight: true + } } } diff --git a/.config/quickshell/ii/services/BluetoothStatus.qml b/.config/quickshell/ii/services/BluetoothStatus.qml index e2d3cee56..73978d634 100644 --- a/.config/quickshell/ii/services/BluetoothStatus.qml +++ b/.config/quickshell/ii/services/BluetoothStatus.qml @@ -12,7 +12,7 @@ import QtQuick Singleton { 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 int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0 readonly property bool connected: Bluetooth.devices.values.some(d => d.connected)