Commit 282a98f9 authored by Nicolas Fella's avatar Nicolas Fella
Browse files

New bluetooth KCM

This is a total rewrite of the bluetooth KCMs with current technology and visual style. It combines the three individual KCMs into a single one using multiple pages.

Functionality-wise it is mostly equivalent, except for two differences.

1) The new KCM does not allow setting an adapter visibility timeout

2) There is no option to not receive files any more. It is somewhat redundant with the default setting of not receiving them automatically.

Adding new devices is not yet ported, instead this just opens the wizard just like the old KCM. We sure want to have it properly integrated at some point, but it's a rather complex piece in itself and the diff is already huge.

Fixes #1
parent d325369e
......@@ -7,6 +7,9 @@ set(PROJECT_VERSION_MAJOR 5)
set(QT_MIN_VERSION "5.14.0")
set(KF5_MIN_VERSION "5.73.0")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
configure_file(version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/version.h)
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
......@@ -28,6 +31,7 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
Plasma
I18n
KIO
Declarative
BluezQt)
find_package(KDED ${KF5_MIN_VERSION} REQUIRED)
......
......@@ -2,10 +2,10 @@ add_definitions(-DTRANSLATION_DOMAIN="bluedevil")
add_subdirectory(sendfile)
add_subdirectory(kded)
add_subdirectory(kcmodule)
add_subdirectory(kio)
add_subdirectory(wizard)
add_subdirectory(applet)
add_subdirectory(kcm)
install(FILES bluedevil.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR})
......
set(kcmbluetooth_SRCS bluetooth.cpp)
kconfig_add_kcfg_files(kcmbluetooth_SRCS GENERATE_MOC
../settings/filereceiversettings.kcfgc)
add_library(kcm_bluetooth MODULE ${kcmbluetooth_SRCS})
target_link_libraries(kcm_bluetooth
Qt5::Gui
Qt5::Qml
Qt5::DBus
KF5::CoreAddons
KF5::ConfigGui
KF5::QuickAddons
KF5::I18n
KF5::BluezQt
)
kcoreaddons_desktop_to_json(kcm_bluetooth "package/metadata.desktop")
install(FILES package/metadata.desktop RENAME bluetooth.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR})
install(TARGETS kcm_bluetooth DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms)
kpackage_install_package(package kcm_bluetooth kcms)
/**
* SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "bluetooth.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <KAboutData>
#include <KLocalizedString>
#include <KPluginFactory>
#include <BluezQt/Services>
#include "filereceiversettings.h"
K_PLUGIN_CLASS_WITH_JSON(Bluetooth, "metadata.json")
Bluetooth::Bluetooth(QObject *parent, const QVariantList &args)
: KQuickAddons::ConfigModule(parent, args)
{
KAboutData *about = new KAboutData("kcm_bluetooth", i18n("Bluetooth"), "1.0", QString(), KAboutLicense::GPL);
about->addAuthor(i18n("Nicolas Fella"), QString(), "nicolas.fella@gmx.de");
setAboutData(about);
setButtons(KQuickAddons::ConfigModule::NoAdditionalButton);
qmlRegisterAnonymousType<QAbstractItemModel>("org.kde.bluedevil.kcm", 1);
qmlRegisterSingletonInstance("org.kde.bluedevil.kcm", 1, 0, "FileReceiverSettings", FileReceiverSettings::self());
}
void Bluetooth::runWizard()
{
QProcess::startDetached(QStringLiteral("bluedevil-wizard"), QStringList());
}
void Bluetooth::runSendFile(const QString &ubi)
{
QProcess::startDetached(QStringLiteral("bluedevil-sendfile"), {QStringLiteral("-u"), ubi});
}
void Bluetooth::checkNetworkConnection(const QStringList &uuids, const QString &address)
{
if (uuids.contains(BluezQt::Services::Nap)) {
checkNetworkInternal(QStringLiteral("nap"), address);
}
if (uuids.contains(BluezQt::Services::DialupNetworking)) {
checkNetworkInternal(QStringLiteral("dun"), address);
}
}
void Bluetooth::checkNetworkInternal(const QString &service, const QString &address)
{
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmanetworkmanagement"),
QStringLiteral("/org/kde/plasmanetworkmanagement"),
QStringLiteral("org.kde.plasmanetworkmanagement"),
QStringLiteral("bluetoothConnectionExists"));
msg << address;
msg << service;
QDBusPendingCallWatcher *call = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(msg));
connect(call, &QDBusPendingCallWatcher::finished, this, [this, service, call]() {
QDBusPendingReply<bool> reply = *call;
if (reply.isError()) {
return;
}
Q_EMIT networkAvailable(service, reply.value());
});
}
void Bluetooth::setupNetworkConnection(const QString &service, const QString &address, const QString &deviceName)
{
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmanetworkmanagement"),
QStringLiteral("/org/kde/plasmanetworkmanagement"),
QStringLiteral("org.kde.plasmanetworkmanagement"),
QStringLiteral("addBluetoothConnection"));
msg << address;
msg << service;
msg << i18nc("DeviceName Network (Service)", "%1 Network (%2)", deviceName, service);
QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
}
#include "bluetooth.moc"
/**
* SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#ifndef BLUETOOTH_H
#define BLUETOOTH_H
#include <KQuickAddons/ConfigModule>
#include <QObject>
class Bluetooth : public KQuickAddons::ConfigModule
{
Q_OBJECT
public:
Bluetooth(QObject *parent, const QVariantList &args);
Q_INVOKABLE void runWizard();
Q_INVOKABLE void runSendFile(const QString &ubi);
Q_INVOKABLE void checkNetworkConnection(const QStringList &uuids, const QString &address);
Q_INVOKABLE void setupNetworkConnection(const QString &service, const QString &address, const QString &deviceName);
Q_SIGNALS:
void networkAvailable(const QString &service, bool available);
private:
void checkNetworkInternal(const QString &service, const QString &address);
};
#endif // BLUETOOTHKCM_H
/**
* SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.10 as QQC2
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kcm 1.2
import org.kde.bluezqt 1.0 as BluezQt
import org.kde.plasma.private.bluetooth 1.0
ScrollViewKCM {
id: root
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;
}
}
header: Kirigami.InlineMessage {
id: errorMessage
type: Kirigami.MessageType.Error
showCloseButton: true
}
view: ListView {
clip: true
Kirigami.PlaceholderMessage {
visible: !BluezQt.Manager.bluetoothOperational
text: i18n("Bluetooth is disabled")
width: parent.width - (Kirigami.Units.largeSpacing * 4)
anchors.centerIn: parent
helpfulAction: Kirigami.Action {
iconName: "network-bluetooth"
text: i18n("Enable")
onTriggered: {
root.setBluetoothEnabled(true)
}
}
}
model: BluezQt.Manager.bluetoothOperational ? devicesModel : []
QQC2.BusyIndicator {
id: busyIndicator
running: false
anchors.centerIn: parent
}
DevicesProxyModel {
id: devicesModel
sourceModel: BluezQt.DevicesModel { }
}
section.property: "Connected"
section.delegate: Kirigami.ListSectionHeader {
text: section === "true" ? i18n("Connected") : i18n("Available")
}
delegate: Kirigami.SwipeListItem {
contentItem: Kirigami.BasicListItem {
// The parent item already has a highlight
activeBackgroundColor: "transparent"
// Otherwise there are unnecessary margins
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
// No right anchor so text can be elided by actions
text: model.Name
icon: model.Icon
onClicked: kcm.push("Device.qml", {device: model.Device})
}
actions: [
Kirigami.Action {
text: model.Connected ? i18n("Disconnect") : i18n("Connect")
icon.name: model.Connected ? "network-disconnect" : "network-connect"
onTriggered: {
if (model.Connected) {
makeCall(model.Device.disconnectFromDevice())
} else {
makeCall(model.Device.connectToDevice())
}
}
},
Kirigami.Action {
text: i18n("Remove")
icon.name: "list-remove"
onTriggered: {
makeCall(model.Adapter.removeDevice(model.Device))
}
}
]
function makeCall(call) {
busyIndicator.running = true
call.finished.connect(call => {
busyIndicator.running = false
if (call.error) {
errorMessage.text = call.errorText
errorMessage.visible = true
}
})
}
}
}
footer: RowLayout {
QQC2.Button {
text: i18n("Add...")
visible: BluezQt.Manager.bluetoothOperational
icon.name: "list-add"
onClicked: kcm.runWizard()
}
QQC2.Button {
visible: BluezQt.Manager.bluetoothOperational
text: i18n("Disable Bluetooth")
icon.name: "network-bluetooth"
onClicked: {
root.setBluetoothEnabled(false)
}
}
Item {
Layout.fillWidth: true
}
QQC2.Button {
visible: BluezQt.Manager.bluetoothOperational
text: i18n("Configure...")
icon.name: "configure"
onClicked: kcm.push("General.qml")
}
}
}
/**
* SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.0 as QQC2
import org.kde.kirigami 2.10 as Kirigami
import org.kde.kcm 1.2
import org.kde.bluezqt 1.0 as BluezQt
SimpleKCM {
property var device
title: device.name
Connections {
target: kcm
function onNetworkAvailable(service, available) {
if (service === "dun") {
dunButton.visible = available && device.connected
}
if (service === "nap") {
napButton.visible = available && device.connected
}
}
}
Connections {
target: device
function onConnectedChanged() {
kcm.checkNetworkConnection(device.uuids, device.address)
}
}
Component.onCompleted: {
kcm.checkNetworkConnection(device.uuids, device.address)
}
header: Kirigami.InlineMessage {
id: errorMessage
type: Kirigami.MessageType.Error
showCloseButton: true
}
ColumnLayout {
Kirigami.Icon {
source: device.icon
width: Kirigami.Units.gridUnit * 4
height: width
Layout.alignment: Qt.AlignHCenter
}
Kirigami.FormLayout {
Row {
QQC2.Button {
id: connectButton
enabled: !indicator.running
text: device.connected ? i18n("Disconnect") : i18n("Connect")
icon.name: device.connected ? "network-disconnect" : "network-connect"
onClicked: {
if (device.connected) {
makeCall(device.disconnectFromDevice())
} else {
makeCall(device.connectToDevice())
}
}
function makeCall(call) {
indicator.running = true
call.finished.connect(call => {
indicator.running = false
if (call.error) {
errorMessage.text = call.errorText
errorMessage.visible = true
}
})
}
}
QQC2.BusyIndicator {
id: indicator
running: false
height: connectButton.height
}
}
QQC2.Label {
text: deviceTypeToString(device.type)
Kirigami.FormData.label: i18n("Type:")
}
QQC2.Label {
text: device.address
Kirigami.FormData.label: i18n("Address:")
}
QQC2.Label {
text: device.adapter.name
Kirigami.FormData.label: i18n("Adapter:")
}
QQC2.TextField {
text: device.name
onTextChanged: device.name = text
Kirigami.FormData.label: i18n("Name:")
}
QQC2.CheckBox {
text: i18n("Trusted")
checked: device.trusted
onClicked: device.trusted = !device.trusted
}
QQC2.CheckBox {
text: i18n("Blocked")
checked: device.blocked
onClicked: device.blocked = !device.blocked
}
QQC2.Button {
text: i18n("Send File")
visible: device.uuids.includes(BluezQt.Services.ObexObjectPush) && device.connected
onClicked: kcm.runSendFile(device.ubi)
}
QQC2.Button {
id: napButton
text: i18n("Setup NAP Network...")
visible: false
onClicked: kcm.setupNetworkConnection("nap", device.address, device.name)
}
QQC2.Button {
id: dunButton
text: i18n("Setup DUN Network...")
visible: false
onClicked: kcm.setupNetworkConnection("dun", device.address, device.name)
}
}
}
function deviceTypeToString(type) {
switch (type) {
case BluezQt.Device.Phone:
return i18nc("This device is a Phone", "Phone");
case BluezQt.Device.Modem:
return i18nc("This device is a Modem", "Modem");
case BluezQt.Device.Computer:
return i18nc("This device is a Computer", "Computer");
case BluezQt.Device.Network:
return i18nc("This device is of type Network", "Network");
case BluezQt.Device.Headset:
return i18nc("This device is a Headset", "Headset");
case BluezQt.Device.Headphones:
return i18nc("This device is a Headphones", "Headphones");
case BluezQt.Device.DeviceAudioVideo:
return i18nc("This device is an Audio device", "Audio");
case BluezQt.Device.Keyboard:
return i18nc("This device is a Keyboard", "Keyboard");
case BluezQt.Device.Mouse:
return i18nc("This device is a Mouse", "Mouse");
case BluezQt.Device.Joypad:
return i18nc("This device is a Joypad", "Joypad");
case BluezQt.Device.Tablet:
return i18nc("This device is a Graphics Tablet (input device)", "Tablet");
case BluezQt.Device.Peripheral:
return i18nc("This device is a Peripheral device", "Peripheral");
case BluezQt.Device.Camera:
return i18nc("This device is a Camera", "Camera");
case BluezQt.Device.Printer:
return i18nc("This device is a Printer", "Printer");
case BluezQt.Device.Imaging:
return i18nc("This device is an Imaging device (printer, scanner, camera, display, ...)", "Imaging");
case BluezQt.Device.Wearable:
return i18nc("This device is a Wearable", "Wearable");
case BluezQt.Device.Toy:
return i18nc("This device is a Toy", "Toy");
case BluezQt.Device.Health:
return i18nc("This device is a Health device", "Health");
default:
return i18nc("Type of device: could not be determined", "Unknown");
}
}
}
/**
* SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.10 as QQC2
import QtQuick.Dialogs 1.3
import org.kde.kirigami 2.10 as Kirigami
import org.kde.kcm 1.2
import org.kde.bluezqt 1.0 as BluezQt
import org.kde.bluedevil.kcm 1.0
SimpleKCM {
title: i18n("Settings")
Kirigami.FormLayout {
id: form
property QtObject adapter: BluezQt.Manager.adapters[box.currentIndex]
QQC2.ComboBox {
id: box
Kirigami.FormData.label: i18n("Device:")
model: BluezQt.Manager.adapters
textRole: "name"
visible: count > 1
}
QQC2.TextField {
text: form.adapter.name
Kirigami.FormData.label: i18n("Name:")
onEditingFinished: form.adapter.name = text
}
QQC2.Label {
text: form.adapter.address
Kirigami.FormData.label: i18n("Address:")
}
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Enabled:")
checked: form.adapter.powered
onToggled: form.adapter.powered = checked
}
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Visible:")
checked: form.adapter.discoverable
onToggled: form.adapter.discoverable = checked
}
Kirigami.Separator {
Kirigami.FormData.isSection: true
}
QQC2.ButtonGroup {
id: radioGroup
}
QQC2.RadioButton {
Kirigami.FormData.label: i18n("When receiving files:")
checked: FileReceiverSettings.autoAccept == 0
text: i18n("Ask for confimation")
QQC2.ButtonGroup.group: radioGroup
onClicked: {
FileReceiverSettings.autoAccept = 0
FileReceiverSettings.save()
}
}
QQC2.RadioButton {
text: i18n("Accept for trusted devices")
checked: FileReceiverSettings.autoAccept == 1