diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c332c4d..8c0f30d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -24,7 +24,7 @@ jobs:
- name: Install Dependencies
run: |
apt update
- apt install -y libglib2.0-dev libgtk-3-dev libwingpanel-dev meson valac
+ apt install -y libglib2.0-dev libgranite-dev libgtk-3-dev libhandy-1-dev libwingpanel-dev meson valac
- name: Build
env:
DESTDIR: out
diff --git a/README.md b/README.md
index 9328e3f..d5f10a0 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,9 @@ Prototype quick settings menu
You'll need the following dependencies:
+* libgtk-3-dev
+* libgranite-dev >=6.0.0
+* libhandy-1-dev >=1.0
* libwingpanel-dev
* meson
* valac
diff --git a/data/icons/bluetooth/active.svg b/data/icons/bluetooth/active.svg
new file mode 100644
index 0000000..fff32c8
--- /dev/null
+++ b/data/icons/bluetooth/active.svg
@@ -0,0 +1,23 @@
+
+
diff --git a/data/icons/bluetooth/disabled.svg b/data/icons/bluetooth/disabled.svg
new file mode 100644
index 0000000..4aa1339
--- /dev/null
+++ b/data/icons/bluetooth/disabled.svg
@@ -0,0 +1,25 @@
+
+
diff --git a/data/icons/bluetooth/paired.svg b/data/icons/bluetooth/paired.svg
new file mode 100644
index 0000000..12d147a
--- /dev/null
+++ b/data/icons/bluetooth/paired.svg
@@ -0,0 +1,19 @@
+
+
diff --git a/data/quick-settings.gresource.xml b/data/quick-settings.gresource.xml
index 7dc2368..52248a6 100644
--- a/data/quick-settings.gresource.xml
+++ b/data/quick-settings.gresource.xml
@@ -8,6 +8,11 @@
icons/dark-mode.svg
icons/quick-settings.svg
+
+ icons/bluetooth/active.svg
+ icons/bluetooth/disabled.svg
+ icons/bluetooth/paired.svg
+
icons/rotation-allowed.svg
icons/rotation-locked.svg
diff --git a/src/DBus/Bluez.vala b/src/DBus/Bluez.vala
new file mode 100644
index 0000000..5d3cd67
--- /dev/null
+++ b/src/DBus/Bluez.vala
@@ -0,0 +1,35 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io)
+ */
+
+[DBus (name = "org.bluez.Adapter1")]
+public interface QuickSettings.Bluez.Adapter : Object {
+ public abstract bool powered { get; set; }
+}
+
+[DBus (name = "org.bluez.Device1")]
+public interface QuickSettings.Bluez.Device : Object {
+ public abstract void cancel_pairing () throws Error;
+ public abstract async void connect () throws Error;
+ public abstract void connect_profile (string UUID) throws Error; //vala-lint=naming-convention
+ public abstract async void disconnect () throws Error;
+ public abstract void disconnect_profile (string UUID) throws Error; //vala-lint=naming-convention
+ public abstract void pair () throws Error;
+
+ public abstract string[] UUIDs { owned get; }
+ public abstract bool blocked { owned get; set; }
+ public abstract bool connected { owned get; }
+ public abstract bool legacy_pairing { owned get; }
+ public abstract bool paired { owned get; }
+ public abstract bool trusted { owned get; set; }
+ public abstract int16 RSSI { owned get; }
+ public abstract ObjectPath adapter { owned get; }
+ public abstract string address { owned get; }
+ public abstract string alias { owned get; set; }
+ public abstract string icon { owned get; }
+ public abstract string modalias { owned get; }
+ public abstract string name { owned get; }
+ public abstract uint16 appearance { owned get; }
+ public abstract uint32 @class { owned get; }
+}
diff --git a/src/PopoverWidget.vala b/src/PopoverWidget.vala
index 6b8feee..d445f53 100644
--- a/src/PopoverWidget.vala
+++ b/src/PopoverWidget.vala
@@ -65,6 +65,16 @@ public class QuickSettings.PopoverWidget : Gtk.Box {
}
});
+ setup_bluetooth.begin ((obj, res) => {
+ var bluetooth_manager = setup_bluetooth.end (res);
+ if (bluetooth_manager != null) {
+ var bluetooth_toggle = new BluetoothToggle (bluetooth_manager);
+
+ toggle_box.add (bluetooth_toggle);
+ show_all ();
+ }
+ });
+
setup_sensor_proxy.begin ((obj, res) => {
var sensor_proxy = setup_sensor_proxy.end (res);
if (sensor_proxy.has_accelerometer) {
@@ -144,6 +154,45 @@ public class QuickSettings.PopoverWidget : Gtk.Box {
}
}
+ //TODO: Do not rely on this when it is possible to do it natively in Vala
+ [CCode (cname="quick_settings_bluez_adapter_proxy_get_type")]
+ extern static GLib.Type get_adapter_proxy_type ();
+
+ //TODO: Do not rely on this when it is possible to do it natively in Vala
+ [CCode (cname="quick_settings_bluez_device_proxy_get_type")]
+ extern static GLib.Type get_device_proxy_type ();
+
+ private GLib.Type object_manager_get_proxy_type (DBusObjectManagerClient manager, string object_path, string? interface_name) {
+ if (interface_name == null) {
+ return typeof (GLib.DBusObjectProxy);
+ }
+
+ switch (interface_name) {
+ case "org.bluez.Device1":
+ return get_device_proxy_type ();
+ case "org.bluez.Adapter1":
+ return get_adapter_proxy_type ();
+ default:
+ return typeof (GLib.DBusProxy);
+ }
+ }
+
+ private async DBusObjectManagerClient? setup_bluetooth () {
+ try {
+ return yield new GLib.DBusObjectManagerClient.for_bus.begin (
+ BusType.SYSTEM,
+ NONE,
+ "org.bluez",
+ "/",
+ object_manager_get_proxy_type,
+ null
+ );
+ } catch (Error e) {
+ critical (e.message);
+ return null;
+ }
+ }
+
private async SensorProxy? setup_sensor_proxy () {
try {
return yield Bus.get_proxy (BusType.SYSTEM, "net.hadess.SensorProxy", "/net/hadess/SensorProxy");
diff --git a/src/Widgets/BluetoothToggle.vala b/src/Widgets/BluetoothToggle.vala
new file mode 100644
index 0000000..d23d163
--- /dev/null
+++ b/src/Widgets/BluetoothToggle.vala
@@ -0,0 +1,141 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io)
+ */
+
+public class QuickSettings.BluetoothToggle: SettingsToggle {
+ public DBusObjectManagerClient bluetooth_manager { get; construct; }
+
+ public BluetoothToggle (DBusObjectManagerClient bluetooth_manager) {
+ Object (
+ bluetooth_manager: bluetooth_manager,
+ icon: new ThemedIcon ("quick-settings-bluetooth-active-symbolic"),
+ label: _("Bluetooth")
+ );
+ }
+
+ construct {
+ settings_uri = "settings://network/bluetooth";
+
+ notify["active"].connect (() => {
+ set_bluetooth_status.begin (active);
+ });
+
+ bluetooth_manager.get_objects ().foreach ((object) => {
+ object.get_interfaces ().foreach ((iface) => on_interface_added (object, iface));
+ });
+ bluetooth_manager.interface_added.connect (on_interface_added);
+ bluetooth_manager.interface_removed.connect (on_interface_removed);
+ bluetooth_manager.object_added.connect ((object) => {
+ object.get_interfaces ().foreach ((iface) => on_interface_added (object, iface));
+ });
+ bluetooth_manager.object_removed.connect ((object) => {
+ object.get_interfaces ().foreach ((iface) => on_interface_removed (object, iface));
+ });
+
+ get_bluetooth_status ();
+ }
+
+ private void on_interface_added (GLib.DBusObject object, GLib.DBusInterface iface) {
+ if (iface is Bluez.Adapter) {
+ unowned var adapter = (Bluez.Adapter) iface;
+
+ ((DBusProxy) adapter).g_properties_changed.connect ((changed, invalid) => {
+ var powered = changed.lookup_value ("Powered", new VariantType ("b"));
+ if (powered != null) {
+ get_bluetooth_status ();
+ }
+ });
+ } else if (iface is Bluez.Device) {
+ unowned var device = (Bluez.Device) iface;
+
+ ((DBusProxy) device).g_properties_changed.connect ((changed, invalid) => {
+ var connected = changed.lookup_value ("Connected", new VariantType ("b"));
+ var paired = changed.lookup_value ("Paired", new VariantType ("b"));
+ if (connected != null || paired != null) {
+ get_bluetooth_status ();
+ }
+ });
+ }
+
+ get_bluetooth_status ();
+ }
+
+ private void on_interface_removed (GLib.DBusObject object, GLib.DBusInterface iface) {
+ get_bluetooth_status ();
+ }
+
+ private void get_bluetooth_status () {
+ var powered = false;
+ foreach (unowned var object in bluetooth_manager.get_objects ()) {
+ DBusInterface? iface = object.get_interface ("org.bluez.Adapter1");
+ if (iface == null) {
+ continue;
+ }
+
+ if (((Bluez.Adapter) iface).powered) {
+ powered = true;
+ break;
+ }
+ }
+
+ if (active != powered) {
+ active = powered;
+ }
+
+ if (powered) {
+ var paired = false;
+ foreach (unowned var object in bluetooth_manager.get_objects ()) {
+ DBusInterface? iface = object.get_interface ("org.bluez.Device1");
+ if (iface == null) {
+ continue;
+ }
+
+ var device = (Bluez.Device) iface;
+ if (device.connected) {
+ paired = true;
+ }
+ }
+
+ if (paired) {
+ icon = new ThemedIcon ("quick-settings-bluetooth-paired-symbolic");
+ } else {
+ icon = new ThemedIcon ("quick-settings-bluetooth-active-symbolic");
+ }
+ } else {
+ icon = new ThemedIcon ("quick-settings-bluetooth-disabled-symbolic");
+ }
+ }
+
+ private async void set_bluetooth_status (bool status) {
+ foreach (unowned var object in bluetooth_manager.get_objects ()) {
+ DBusInterface? iface = object.get_interface ("org.bluez.Adapter1");
+ if (iface == null) {
+ continue;
+ }
+
+ var adapter = (Bluez.Adapter) iface;
+ if (adapter.powered != status) {
+ adapter.powered = status;
+ }
+ }
+
+ if (!status) {
+ foreach (unowned var object in bluetooth_manager.get_objects ()) {
+ DBusInterface? iface = object.get_interface ("org.bluez.Device1");
+ if (iface == null) {
+ continue;
+ }
+
+ var device = (Bluez.Device) iface;
+ if (device.connected) {
+ try {
+ yield device.disconnect ();
+ } catch (Error e) {
+ critical (e.message);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index b969b19..913f7e6 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -11,8 +11,10 @@ sources = [
'Indicator.vala',
'PopoverWidget.vala',
'DBus' / 'AccountsService.vala',
+ 'DBus' / 'Bluez.vala',
'DBus' / 'SensorProxy.vala',
'Views' / 'A11yView.vala',
+ 'Widgets' / 'BluetoothToggle.vala',
'Widgets' / 'DarkModeToggle.vala',
'Widgets' / 'RotationToggle.vala',
'Widgets' / 'SettingsToggle.vala'