diff --git a/Modules/BluetoothPanel/BluetoothDevicesList.qml b/Modules/BluetoothPanel/BluetoothDevicesList.qml index 9d4f981..d234393 100644 --- a/Modules/BluetoothPanel/BluetoothDevicesList.qml +++ b/Modules/BluetoothPanel/BluetoothDevicesList.qml @@ -12,6 +12,7 @@ ColumnLayout { id: root property string label: "" + property string tooltipText: "" property var model: { } @@ -29,12 +30,15 @@ ColumnLayout { } Repeater { + id: deviceList Layout.fillWidth: true model: root.model visible: BluetoothService.adapter && BluetoothService.adapter.enabled Rectangle { + id: bluetoothDeviceRectangle property bool canConnect: BluetoothService.canConnect(modelData) + property bool canDisconnect: BluetoothService.canDisconnect(modelData) property bool isBusy: BluetoothService.isDeviceBusy(modelData) Layout.fillWidth: true @@ -42,13 +46,19 @@ ColumnLayout { radius: Style.radiusM * scaling color: { - if (availableDeviceArea.containsMouse && !isBusy) - return Color.mTertiary + if (availableDeviceArea.containsMouse){ + if (canDisconnect && !isBusy) + return Color.mError + + if(!isBusy) + return Color.mTertiary + return Color.mPrimary + } if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) return Color.mPrimary - if (modelData.blocked) + if (modelData.blocked ) return Color.mError return Color.mSurfaceVariant @@ -56,6 +66,13 @@ ColumnLayout { border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) + NTooltip { + id: tooltip + target: bluetoothDeviceRectangle + positionAbove: Settings.data.bar.position === "bottom" + text: root.tooltipText + } + RowLayout { anchors.fill: parent anchors.margins: Style.marginM * scaling @@ -193,6 +210,7 @@ ColumnLayout { Layout.fillWidth: true } + // Call to action Rectangle { Layout.preferredWidth: 80 * scaling @@ -204,12 +222,14 @@ ColumnLayout { border.color: { if (availableDeviceArea.containsMouse) { return Color.mOnTertiary - } else { - return Color.mPrimary } + if (bluetoothDeviceRectangle.canDisconnect && !isBusy) { + return Color.mError + } + return Color.mPrimary } border.width: Math.max(1, Style.borderS * scaling) - opacity: canConnect || isBusy ? 1 : 0.5 + opacity: canConnect || isBusy || canDisconnect ? 1 : 0.5 NText { anchors.centerIn: parent @@ -220,7 +240,7 @@ ColumnLayout { if (modelData.blocked) { return "Blocked" } - if (modelData.paired || modelData.trusted) { + if(modelData.connected){ return "Disconnect" } return "Connect" @@ -228,8 +248,13 @@ ColumnLayout { font.pointSize: Style.fontSizeXS * scaling font.weight: Style.fontWeightMedium color: { + if (availableDeviceArea.containsMouse) { return Color.mOnTertiary + } + + if (bluetoothDeviceRectangle.canDisconnect && !isBusy) { + return Color.mError } else { return Color.mPrimary } @@ -239,21 +264,41 @@ ColumnLayout { } MouseArea { + id: availableDeviceArea - + acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent hoverEnabled: true - cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) - enabled: canConnect && !isBusy - onClicked: { + cursorShape: (canConnect || canDisconnect) && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) + onEntered: { + if (root.tooltipText && !isBusy) { + tooltip.show() + } + } + onExited: { + if(root.tooltipText && !isBusy) { + tooltip.hide() + } + } + onClicked: function(mouse) { + if (!modelData || modelData.pairing) { return } - if (modelData.paired || modelData.trusted) { - BluetoothService.disconnectDevice(modelData) - } else { - BluetoothService.connectDeviceWithTrust(modelData) + if (root.tooltipText && !isBusy) { + tooltip.hide() + } + + + if (mouse.button === Qt.LeftButton){ + if (modelData.connected) { + BluetoothService.disconnectDevice(modelData) + } else { + BluetoothService.connectDeviceWithTrust(modelData) + } + } else if (mouse.button === Qt.RightButton) { + BluetoothService.forgetDevice(modelData) } } } diff --git a/Modules/BluetoothPanel/BluetoothPanel.qml b/Modules/BluetoothPanel/BluetoothPanel.qml index 77ed79c..8dfcf47 100644 --- a/Modules/BluetoothPanel/BluetoothPanel.qml +++ b/Modules/BluetoothPanel/BluetoothPanel.qml @@ -83,30 +83,40 @@ NPanel { // Connected devices BluetoothDevicesList { label: "Connected devices" - model: { - if (!BluetoothService.adapter || !Bluetooth.devices) - return [] - - var filtered = Bluetooth.devices.values.filter(dev => { - return dev && !dev.blocked && (dev.paired || dev.trusted) - }) + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) return [] + var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected) return BluetoothService.sortDevices(filtered) } + model: items + visible: items.length > 0 + Layout.fillWidth: true + } + + // Known devices + BluetoothDevicesList { + label: "Known devices" + tooltipText: "Left click to connect, right click to forget" + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) return [] + var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted)) + return BluetoothService.sortDevices(filtered) + } + model: items + visible: items.length > 0 Layout.fillWidth: true } // Available devices BluetoothDevicesList { label: "Available devices" - model: { - if (!BluetoothService.adapter || !Bluetooth.devices) - return [] - - var filtered = Bluetooth.devices.values.filter(dev => { - return dev && !dev.blocked && !dev.paired && !dev.trusted - }) + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) return [] + var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted) return BluetoothService.sortDevices(filtered) } + model: items + visible: items.length > 0 Layout.fillWidth: true } diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 2e88f5b..98c8053 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Bluetooth +import qs.Commons Singleton { id: root @@ -85,8 +86,23 @@ Singleton { function canConnect(device) { if (!device) return false + /* + Paired - return !device.paired && !device.pairing && !device.blocked + Means you’ve successfully exchanged keys with the device. + + The devices remember each other and can authenticate without repeating the pairing process. + + Example: once your headphones are paired, you don’t need to type a PIN every time. + Hence, instead of !device.paired, should be device.connected + */ + return !device.connected && !device.pairing && !device.blocked + } + + function canDisconnect(device) { + if (!device) + return false + return device.connected && !device.pairing && !device.blocked } function getSignalStrength(device) { @@ -162,7 +178,6 @@ Singleton { return } - device.trusted = false device.disconnect() } diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 2295451..c66c1f4 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -11,7 +11,7 @@ Singleton { property list ddcMonitors: [] readonly property list monitors: variants.instances property bool appleDisplayPresent: false - + function getMonitorForScreen(screen: ShellScreen): var { return monitors.find(m => m.modelData === screen) } @@ -69,19 +69,35 @@ Singleton { // Detect DDC monitors Process { id: ddcProc - command: ["ddcutil", "detect", "--brief"] + property list ddcMonitors: [] + command: ["ddcutil", "detect", "--sleep-multiplier=0.5"] stdout: StdioCollector { onStreamFinished: { // Do not filter out invalid displays. For some reason --brief returns some invalid which works fine var displays = text.trim().split("\n\n") - root.ddcMonitors = displays.map(d => { - var modelMatch = d.match(/Monitor:.*:(.*):.*/) + + + ddcProc.ddcMonitors = displays.map(d => { + + var ddcModelMatc = d.match(/This monitor does not support DDC\/CI/) + var modelMatch = d.match(/Model:\s*(.*)/) var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) + var ddcModel = ddcModelMatc ? ddcModelMatc.length > 0 : false + var model = modelMatch ? modelMatch[1] : "Unknown" + var bus = busMatch ? busMatch[1] : "Unknown" + Logger.log( + "Detected DDC Monitor:", model, + "on bus", bus, "is DDC:", !ddcModel + ) return { - "model": modelMatch ? modelMatch[1] : "", - "busNum": busMatch ? busMatch[1] : "" + "model": model, + "busNum": bus, + "isDdc": !ddcModel, } }) + root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc) + + } } }