diff --git a/debian/control b/debian/control index 7be2f5d..607167e 100644 --- a/debian/control +++ b/debian/control @@ -31,8 +31,6 @@ Depends: qml6-module-qtquick-controls, qml6-module-qtquick-dialogs, qml6-module-qtquick-particles, qml6-module-qt5compat-graphicaleffects, - qml6-module-org-kde-bluezqt, - bluedevil, network-manager, fishui, libcutefish, diff --git a/src/qml/Bluetooth/Main.qml b/src/qml/Bluetooth/Main.qml index 0f23c3c..490a93f 100644 --- a/src/qml/Bluetooth/Main.qml +++ b/src/qml/Bluetooth/Main.qml @@ -17,10 +17,9 @@ * along with this program. If not, see . */ -import QtQuick 2.4 -import QtQuick.Controls 2.4 -import QtQuick.Layouts 1.3 -import Cutefish.Settings 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import FishUI 1.0 as FishUI import Cutefish.Bluetooth 1.0 as Bluetooth import "../" @@ -28,51 +27,43 @@ import "../" ItemPage { id: control headerTitle: qsTr("Bluetooth") + readonly property bool bluetoothEnabled: Bluetooth.Manager.enabled - property bool bluetoothDisConnected: Bluetooth.Manager.bluetoothBlocked - - onBluetoothDisConnectedChanged: { - bluetoothSwitch.checked = !bluetoothDisConnected - } - - function setBluetoothEnabled(enabled) { - Bluetooth.Manager.bluetoothBlocked = !enabled - - for (var i = 0; i < Bluetooth.Manager.adapters.length; ++i) { - var adapter = Bluetooth.Manager.adapters[i] - adapter.powered = enabled - } + function updateActivity() { + Bluetooth.Manager.agentEnabled = visible + Bluetooth.Manager.discoveryRequested = visible && Bluetooth.Manager.enabled } - Bluetooth.DevicesProxyModel { - id: devicesProxyModel - sourceModel: devicesModel + onVisibleChanged: updateActivity() + onBluetoothEnabledChanged: { + Bluetooth.Manager.discoveryRequested = visible && bluetoothEnabled } - - Bluetooth.DevicesModel { - id: devicesModel + Component.onCompleted: updateActivity() + Component.onDestruction: { + Bluetooth.Manager.discoveryRequested = false + Bluetooth.Manager.agentEnabled = false } - Bluetooth.BluetoothManager { - id: bluetoothMgr - - onShowPairDialog: { - _pairDialog.title = name - _pairDialog.pin = pin - _pairDialog.visible = true + Bluetooth.DevicesModel { id: devicesModel } + PairDialog { } + + Dialog { + id: forgetDialog + property var device: null + title: qsTr("Forget this device?") + anchors.centerIn: parent + modal: true + standardButtons: Dialog.Cancel | Dialog.Ok + Label { + text: forgetDialog.device ? forgetDialog.device.name : "" + textFormat: Text.PlainText } - - onPairFailed: { - rootWindow.showPassiveNotification(qsTr("Pairing unsuccessful"), 3000) - } - - onConnectFailed: { - rootWindow.showPassiveNotification(qsTr("Connecting Unsuccessful"), 3000) + onAccepted: { + if (device) + device.forget() + device = null } - } - - PairDialog { - id: _pairDialog + onRejected: device = null } Scrollable { @@ -81,188 +72,200 @@ ItemPage { ColumnLayout { id: layout - anchors.fill: parent - anchors.bottomMargin: FishUI.Units.largeSpacing + width: parent.contentWidth + spacing: FishUI.Units.largeSpacing RoundedItem { - id: mainItem - spacing: FishUI.Units.largeSpacing - RowLayout { Label { text: qsTr("Bluetooth") - color: FishUI.Theme.disabledTextColor - } - - Item { Layout.fillWidth: true } - Switch { - id: bluetoothSwitch - Layout.fillHeight: true - rightPadding: 0 - checked: !Bluetooth.Manager.bluetoothBlocked - onCheckedChanged: setBluetoothEnabled(checked) + checked: Bluetooth.Manager.enabled + enabled: Bluetooth.Manager.operational && Bluetooth.Manager.adapters.length > 0 + onClicked: Bluetooth.Manager.enabled = checked } } - - ListView { - id: _listView - visible: count > 0 - interactive: false - spacing: 0 - + Label { Layout.fillWidth: true - - Layout.preferredHeight: { - var totalHeight = 0 - for (var i = 0; i < _listView.visibleChildren.length; ++i) { - totalHeight += _listView.visibleChildren[i].height + wrapMode: Text.Wrap + visible: !Bluetooth.Manager.operational || !Bluetooth.Manager.adapters.length + || Bluetooth.Manager.hardwareBlocked || !Bluetooth.Manager.enabled + text: !Bluetooth.Manager.operational ? qsTr("Bluetooth service is unavailable") + : Bluetooth.Manager.hardwareBlocked ? qsTr("Bluetooth is blocked by a hardware switch") + : !Bluetooth.Manager.adapters.length ? qsTr("No Bluetooth adapters found") + : qsTr("Bluetooth is disabled") + } + RowLayout { + visible: Bluetooth.Manager.enabled + Label { + Layout.fillWidth: true + text: Bluetooth.Manager.scanning ? qsTr("Searching for devices…") : qsTr("Devices") + } + BusyIndicator { + running: Bluetooth.Manager.scanning + implicitWidth: 28 + implicitHeight: 28 + } + Button { + text: Bluetooth.Manager.discoveryRequested ? qsTr("Stop searching") : qsTr("Search") + onClicked: { + Bluetooth.Manager.clearError() + Bluetooth.Manager.discoveryRequested = !Bluetooth.Manager.discoveryRequested } - return totalHeight } + } + } - model: Bluetooth.Manager.bluetoothOperational ? devicesProxyModel : [] + RoundedItem { + visible: Bluetooth.Manager.errorText.length > 0 + Label { + Layout.fillWidth: true + wrapMode: Text.Wrap + textFormat: Text.PlainText + text: Bluetooth.Manager.errorText + } + Button { + text: qsTr("Dismiss") + onClicked: Bluetooth.Manager.clearError() + } + } - section.property: "Section" - section.criteria: ViewSection.FullString - section.delegate: Label { - color: FishUI.Theme.disabledTextColor - topPadding: FishUI.Units.largeSpacing - bottomPadding: FishUI.Units.largeSpacing - text: section == "My devices" ? qsTr("My devices") - : qsTr("Other devices") + Repeater { + model: Bluetooth.Manager.adapters + delegate: RoundedItem { + required property var modelData + Label { + Layout.fillWidth: true + text: modelData.name + " · " + modelData.address + textFormat: Text.PlainText + elide: Text.ElideRight } + RowLayout { + Layout.fillWidth: true + TextField { + id: adapterName + Layout.fillWidth: true + placeholderText: modelData.name + maximumLength: 248 + } + Button { + text: qsTr("Rename") + enabled: adapterName.text.length > 0 + onClicked: { + modelData.name = adapterName.text + adapterName.clear() + } + } + } + RowLayout { + Switch { + text: qsTr("Powered") + checked: modelData.powered + onClicked: modelData.powered = checked + } + Switch { + text: qsTr("Visible to other devices") + enabled: modelData.powered + checked: modelData.discoverable + onClicked: modelData.discoverable = checked + } + } + Switch { + text: qsTr("Allow incoming pairing") + enabled: modelData.powered + checked: modelData.pairable + onClicked: modelData.pairable = checked + } + } + } - delegate: Item { - width: ListView.view.width - height: _itemLayout.implicitHeight + FishUI.Units.largeSpacing - - property bool paired: model.Connected && model.Paired + Label { + visible: Bluetooth.Manager.enabled && devicesModel.count === 0 + text: qsTr("No devices found. Make your device discoverable and search again.") + wrapMode: Text.Wrap + Layout.fillWidth: true + } + Repeater { + model: devicesModel + delegate: RoundedItem { + required property var device + visible: Bluetooth.Manager.enabled + RowLayout { ColumnLayout { - id: _itemLayout - anchors.fill: parent - anchors.leftMargin: 0 - anchors.rightMargin: 0 - anchors.topMargin: FishUI.Units.smallSpacing - anchors.bottomMargin: FishUI.Units.smallSpacing - spacing: 0 - - Item { + Layout.fillWidth: true + Label { Layout.fillWidth: true - height: _contentLayout.implicitHeight + FishUI.Units.largeSpacing - - Rectangle { - anchors.fill: parent - radius: FishUI.Theme.smallRadius - color: FishUI.Theme.textColor - opacity: mouseArea.pressed ? 0.15 : mouseArea.containsMouse ? 0.1 : 0.0 - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - - onClicked: { - if (model.Connected || model.Paired){ - additionalSettings.toggle() - return - } - - if (model.Paired) { - bluetoothMgr.connectToDevice(model.Address) - } else { - bluetoothMgr.requestParingConnection(model.Address) - } - } - } - - RowLayout { - id: _contentLayout - anchors.fill: parent - anchors.rightMargin: FishUI.Units.smallSpacing - - Image { - width: 16 - height: 16 - sourceSize: Qt.size(16, 16) - source: FishUI.Theme.darkMode ? "qrc:/images/sidebar/dark/bluetooth.svg" - : "qrc:/images/sidebar/light/bluetooth.svg" - Layout.alignment: Qt.AlignVCenter - } - - Label { - text: model.DeviceFullName - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - - Label { - visible: model.Paired - text: model.Connected ? qsTr("Connected") : qsTr("Not Connected") - } - } + text: device.name + textFormat: Text.PlainText + elide: Text.ElideRight } - - Hideable { - id: additionalSettings - spacing: 0 - - ColumnLayout { - Item { - height: FishUI.Units.largeSpacing - } - - RowLayout { - spacing: FishUI.Units.largeSpacing - Layout.leftMargin: FishUI.Units.smallSpacing - - Button { - text: qsTr("Connect") - visible: !model.Connected - onClicked: { - if (model.Paired) { - bluetoothMgr.connectToDevice(model.Address) - } else { - bluetoothMgr.requestParingConnection(model.Address) - } - } - } - - Button { - text: qsTr("Disconnect") - visible: model.Connected - onClicked: { - bluetoothMgr.deviceDisconnect(model.Address) - additionalSettings.hide() - } - } - - Button { - text: qsTr("Forget This Device") - flat: true - onClicked: { - bluetoothMgr.deviceRemoved(model.Address) - additionalSettings.hide() - } - } - } - } - - HorizontalDivider {} + Label { + text: device.address + " · " + device.adapterPath.split("/").pop() + color: FishUI.Theme.disabledTextColor + } + Label { + text: device.connected ? qsTr("Connected") + : device.paired ? qsTr("Paired") : qsTr("Not paired") } } + Label { + visible: device.batteryPercentage >= 0 + text: qsTr("%1% battery").arg(device.batteryPercentage) + } + BusyIndicator { + running: device.busy + visible: running + implicitWidth: 28 + implicitHeight: 28 + } + } + RowLayout { + Button { + text: device.connected ? qsTr("Disconnect") + : device.paired ? qsTr("Connect") : qsTr("Pair") + enabled: !device.busy && (device.paired || Bluetooth.Manager.agentReady) + onClicked: { + if (device.connected) + device.disconnectFromDevice() + else if (device.paired) + device.connectToDevice() + else + device.pair() + } + } + Button { + visible: device.busy && !device.paired + text: qsTr("Cancel pairing") + onClicked: device.cancelPairing() + } + Button { + visible: device.paired + enabled: !device.busy + text: qsTr("Forget") + onClicked: { + forgetDialog.device = device + forgetDialog.open() + } + } + CheckBox { + visible: device.paired + text: qsTr("Trusted") + checked: device.trusted + enabled: !device.busy + onClicked: device.trusted = checked + } + CheckBox { + text: qsTr("Blocked") + checked: device.blocked + enabled: !device.busy + onClicked: device.blocked = checked + } } } } - - Item { - height: FishUI.Units.largeSpacing * 2 - } } } } diff --git a/src/qml/Bluetooth/PairDialog.qml b/src/qml/Bluetooth/PairDialog.qml index 16cefc4..2333949 100644 --- a/src/qml/Bluetooth/PairDialog.qml +++ b/src/qml/Bluetooth/PairDialog.qml @@ -1,103 +1,93 @@ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.4 -import QtQuick.Layouts 1.3 -import Cutefish.Settings 1.0 -import FishUI 1.0 as FishUI -import "../" - -FishUI.Window { - id: control - - windowRadius: 16 - windowButtonsTopMargin: 8 - windowButtonsRightMargin: 8 - minimizeButtonVisible: false - header.height: 44 - contentTopMargin: 0 - - width: contentWidth - height: contentHeight - - property int contentWidth: 360 - property int contentHeight: mainLayout.implicitHeight + 32 - - minimumWidth: contentWidth - minimumHeight: contentHeight - maximumWidth: contentWidth - maximumHeight: contentHeight - - modality: Qt.WindowModal - flags: Qt.Dialog | Qt.FramelessWindowHint - visible: false - title: " " - - property var pin: "" - - background.color: FishUI.Theme.secondBackgroundColor - - DragHandler { - target: null - acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad - grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType | PointerHandler.ApprovesTakeOverByAnything - onActiveChanged: if (active) { control.helper.startSystemMove(control) } +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Cutefish.Bluetooth 1.0 as Bluetooth + +Dialog { + id: dialog + anchors.centerIn: parent + width: Math.min(parent.width - 32, 420) + modal: true + closePolicy: Popup.NoAutoClose + title: qsTr("Bluetooth pairing") + + readonly property string kind: Bluetooth.Manager.pairingKind + readonly property bool inputRequired: kind === "RequestPinCode" || kind === "RequestPasskey" + readonly property bool displayOnly: kind === "DisplayPinCode" || kind === "DisplayPasskey" + + Connections { + target: Bluetooth.Manager + function onPairingChanged() { + if (Bluetooth.Manager.pairingKind.length) { + if (!dialog.opened) { + codeInput.clear() + dialog.open() + if (dialog.inputRequired) + codeInput.forceActiveFocus() + } + } else { + dialog.close() + } + } } - ColumnLayout { - id: mainLayout - anchors.fill: parent - anchors.margins: 16 + contentItem: ColumnLayout { spacing: 16 - Label { Layout.fillWidth: true - Layout.rightMargin: 32 - text: "Bluetooth Pairing Request" + text: Bluetooth.Manager.pairingDevice textFormat: Text.PlainText - font.pixelSize: 14 - font.weight: Font.DemiBold - color: FishUI.Theme.textColor wrapMode: Text.Wrap } - Label { Layout.fillWidth: true - text: control.title.trim() - textFormat: Text.PlainText - font.pixelSize: 14 - color: FishUI.Theme.disabledTextColor + text: dialog.kind === "RequestConfirmation" ? qsTr("Does this code match the code on your device?") + : dialog.kind === "RequestPinCode" ? qsTr("Enter the PIN for your device.") + : dialog.kind === "RequestPasskey" ? qsTr("Enter the passkey shown on your device.") + : dialog.displayOnly ? qsTr("Enter this code on your device, then press Enter.") + : dialog.kind === "AuthorizeService" ? qsTr("Allow this device to use the following Bluetooth service?") + : qsTr("Allow this device to pair?") wrapMode: Text.Wrap - visible: text.length > 0 } - Label { - text: control.pin + Layout.fillWidth: true + visible: Bluetooth.Manager.pairingCode.length > 0 + text: Bluetooth.Manager.pairingCode textFormat: Text.PlainText - visible: control.pin !== "" - font.pixelSize: 24 - font.weight: Font.DemiBold - Layout.alignment: Qt.AlignHCenter + wrapMode: Text.WrapAnywhere + font.pixelSize: dialog.kind === "AuthorizeService" ? 14 : 28 + horizontalAlignment: Text.AlignHCenter + } + Label { + visible: dialog.kind === "DisplayPasskey" + text: qsTr("%1 digits entered").arg(Bluetooth.Manager.pairingEntered) + } + TextField { + id: codeInput + Layout.fillWidth: true + visible: dialog.inputRequired + maximumLength: dialog.kind === "RequestPasskey" ? 6 : 16 + inputMethodHints: dialog.kind === "RequestPasskey" ? Qt.ImhDigitsOnly : Qt.ImhNone + onAccepted: Bluetooth.Manager.respondPairing(true, text) + } + Label { + Layout.fillWidth: true + visible: Bluetooth.Manager.errorText.length > 0 + text: Bluetooth.Manager.errorText + textFormat: Text.PlainText + wrapMode: Text.Wrap } - RowLayout { - spacing: 8 Layout.alignment: Qt.AlignRight - - FishUI.ConfirmDialogButton { + Button { text: qsTr("Cancel") - onClicked: { - control.visible = false - bluetoothMgr.confirmMatchButton(false) - } + onClicked: Bluetooth.Manager.cancelPairing() } - - FishUI.ConfirmDialogButton { - text: qsTr("OK") - primary: true - onClicked: { - control.visible = false - bluetoothMgr.confirmMatchButton(true) - } + Button { + visible: !dialog.displayOnly + text: dialog.kind === "RequestConfirmation" ? qsTr("Confirm") : qsTr("Accept") + enabled: !dialog.inputRequired || codeInput.text.length > 0 + onClicked: Bluetooth.Manager.respondPairing(true, codeInput.text) } } } diff --git a/src/qml/BluetoothPage.qml b/src/qml/BluetoothPage.qml index b4269ff..0930151 100644 --- a/src/qml/BluetoothPage.qml +++ b/src/qml/BluetoothPage.qml @@ -17,84 +17,7 @@ * along with this program. If not, see . */ -import QtQuick 2.4 -import QtQuick.Controls 2.4 -import QtQuick.Layouts 1.3 -import Cutefish.Settings 1.0 -import FishUI 1.0 as FishUI -import org.kde.bluezqt 1.0 as BluezQt -import org.kde.plasma.private.bluetooth 1.0 +import QtQuick +import "Bluetooth" as BluetoothPage -ItemPage { - headerTitle: qsTr("Bluetooth") - - function setBluetoothEnabled(enabled) { - BluezQt.Manager.bluetoothBlocked = !enabled - - for (var i = 0; i < BluezQt.Manager.adapters.length; ++i) { - var adapter = BluezQt.Manager.adapters[i]; - adapter.powered = enabled; - } - } - -// Label { -// id: noBluetoothMessage -// anchors.centerIn: parent -// visible: BluezQt.Manager.rfkill.state === BluezQt.Rfkill.Unknown -// text: qsTr("No Bluetooth adapters found") -// } - - Label { - anchors.centerIn: parent - text: qsTr("Bluetooth is disabled") - visible: BluezQt.Manager.operational && !BluezQt.Manager.bluetoothOperational // && !noBluetoothMessage.visible - } - - Scrollable { - anchors.fill: parent - contentHeight: layout.implicitHeight - - ColumnLayout { - id: layout - anchors.fill: parent - anchors.bottomMargin: FishUI.Units.largeSpacing - - Switch { - id: bluetoothSwitch - height: 50 - onCheckedChanged: setBluetoothEnabled(checked) - Component.onCompleted: bluetoothSwitch.checked = BluezQt.Manager.operational - } - - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - - DevicesProxyModel { - id: devicesModel - sourceModel: BluezQt.DevicesModel { } - } - - model: BluezQt.Manager.bluetoothOperational ? devicesModel : [] - - delegate: Item { - width: ListView.view.width - height: 50 - - Rectangle { - anchors.fill: parent - color: "red" - } - - ColumnLayout { - anchors.fill: parent - - Label { - text: model.name - } - } - } - } - } - } -} +BluetoothPage.Main { } diff --git a/src/qml/SideBar.qml b/src/qml/SideBar.qml index e557c83..cdc35e6 100644 --- a/src/qml/SideBar.qml +++ b/src/qml/SideBar.qml @@ -23,8 +23,10 @@ import QtQuick.Layouts 1.3 import FishUI 1.0 as FishUI import Cutefish.Settings 1.0 import Cutefish.Network 1.0 as NM +import Cutefish.Bluetooth 1.0 as Bluetooth Item { + id: sideBar implicitWidth: 230 property int itemRadiusV: 8 @@ -32,6 +34,67 @@ Item { property alias view: listView property alias model: listModel property alias currentIndex: listView.currentIndex + property bool updatingModel: false + signal currentPageRemoved() + signal bluetoothItemAdded() + + Component.onCompleted: syncBluetoothItem() + + Connections { + target: Bluetooth.Manager + function onAvailableChanged() { + Qt.callLater(sideBar.syncBluetoothItem) + } + } + + function syncBluetoothItem() { + var bluetoothIndex = -1 + for (var i = 0; i < listModel.count; ++i) { + if (listModel.get(i).name === "bluetooth") { + bluetoothIndex = i + break + } + } + if (Bluetooth.Manager.available === (bluetoothIndex >= 0)) + return + + var selectedPage = listView.currentIndex >= 0 + ? listModel.get(listView.currentIndex).page : "" + var selectedRemoved = bluetoothIndex >= 0 && listView.currentIndex === bluetoothIndex + updatingModel = true + if (Bluetooth.Manager.available) { + var insertIndex = 0 + while (insertIndex < listModel.count + && (listModel.get(insertIndex).name === "wlan" + || listModel.get(insertIndex).name === "ethernet")) + ++insertIndex + listModel.insert(insertIndex, { + title: qsTr("Bluetooth"), + name: "bluetooth", + page: "qrc:/qml/Bluetooth/Main.qml", + iconSource: "bluetooth.svg", + iconColor: "#0067FF", + category: qsTr("Network and connection") + }) + } else { + listModel.remove(bluetoothIndex) + } + if (selectedRemoved) { + listView.currentIndex = 0 + } else { + for (var j = 0; j < listModel.count; ++j) { + if (listModel.get(j).page === selectedPage) { + listView.currentIndex = j + break + } + } + } + updatingModel = false + if (selectedRemoved) + currentPageRemoved() + else if (Bluetooth.Manager.available) + bluetoothItemAdded() + } Rectangle { anchors.fill: parent @@ -77,15 +140,6 @@ Item { // category: qsTr("Network and connection") // } - ListElement { - title: qsTr("Bluetooth") - name: "bluetooth" - page: "qrc:/qml/Bluetooth/Main.qml" - iconSource: "bluetooth.svg" - iconColor: "#0067FF" - category: qsTr("Network and connection") - } - // ListElement { // title: qsTr("VPN") // name: "vpn" diff --git a/src/qml/main.qml b/src/qml/main.qml index bbe912e..a12c371 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -34,6 +34,7 @@ FishUI.Window { minimumHeight: 600 property alias stackView: _stackView + property string pendingPageName: "" background.color: FishUI.Theme.backgroundColor background.opacity: blurEnabled && blurAvailable ? (FishUI.Theme.darkMode ? 0.7 : 0.5) : 1 @@ -58,7 +59,13 @@ FishUI.Window { : Math.round(implicitWidth * pixelRatio) / pixelRatio onCurrentIndexChanged: { - switchPageFromIndex(currentIndex) + if (!sideBar.updatingModel) + switchPageFromIndex(currentIndex) + } + onCurrentPageRemoved: switchPageFromIndex(currentIndex) + onBluetoothItemAdded: { + if (rootWindow.pendingPageName === "bluetooth") + rootWindow.switchPageFromName("bluetooth") } } @@ -77,11 +84,15 @@ FishUI.Window { } function switchPageFromIndex(index) { + if (index < 0 || index >= sideBar.model.count) + return + pendingPageName = "" _stackView.pop() _stackView.push(Qt.resolvedUrl(sideBar.model.get(index).page)) } function switchPageFromName(pageName) { + pendingPageName = pageName === "bluetooth" ? pageName : "" for (var i = 0; i < sideBar.model.count; ++i) { if (pageName === sideBar.model.get(i).name) { switchPageFromIndex(i)