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'