From ba0d60beba37011499ca24cb24f9da6cc100759d Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 25 Sep 2022 21:44:59 -0400 Subject: [PATCH 01/81] Implement the private portion of the Bluetooth API (I hope... The organization is a mess.) Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 1081 +++++++++++++++++ src/panel/applets/status/BluetoothDBus.vala | 75 ++ src/panel/applets/status/BluetoothDevice.vala | 96 ++ src/panel/applets/status/BluetoothEnums.vala | 84 ++ .../applets/status/BluetoothIndicator.vala | 68 +- src/panel/applets/status/meson.build | 9 +- vapi/README.md | 4 + vapi/UPowerGlib-1.0-custom.vala | 5 + vapi/UPowerGlib-1.0.metadata | 5 + vapi/upower-glib.vapi | 319 +++-- 10 files changed, 1499 insertions(+), 247 deletions(-) create mode 100644 src/panel/applets/status/BluetoothClient.vala create mode 100644 src/panel/applets/status/BluetoothDBus.vala create mode 100644 src/panel/applets/status/BluetoothDevice.vala create mode 100644 src/panel/applets/status/BluetoothEnums.vala create mode 100644 vapi/UPowerGlib-1.0-custom.vala create mode 100644 vapi/UPowerGlib-1.0.metadata diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala new file mode 100644 index 000000000..5286e9cdd --- /dev/null +++ b/src/panel/applets/status/BluetoothClient.vala @@ -0,0 +1,1081 @@ +/* + * This file is part of budgie-desktop + * + * Copyright © 2015-2022 Budgie Desktop Developers + * Copyright (C) 2015 Alberts Muktupāvels + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Inspired by gnome-bluetooth. + */ + +using GLib; +using Up; + +const string BLUEZ_DBUS_NAME = "org.bluez"; +const string BLUEZ_MANAGER_PATH = "/"; +const string BLUEZ_ADAPTER_INTERFACE = "org.bluez.Adapter1"; +const string BLUEZ_DEVICE_INTERFACE = "org.bluez.Device1"; + +const uint DEVICE_REMOVAL_TIMEOUT = 50; + +enum AdapterChangeType { + OWNER_UPDATE, + REPLACEMENT, + NEW_DEFAULT, + REMOVED +} + +[DBus (name="org.freedesktop.DBus.ObjectManager")] +public interface BluezManager : GLib.Object { + public abstract HashTable>> GetManagedObjects() throws GLib.DBusError, GLib.IOError; +} + +class BluetoothClient : GLib.Object { + Cancellable cancellable; + ListStore list_store; + + DBusObjectManagerClient dbus_object_manager = null; + + public uint num_adapters { get; private set; default = 0; } + public Adapter1 default_adapter { get; private set; default = null; } + public PowerState default_adapter_state { get; private set; default = ABSENT; } + public bool discovery_started { get; set; default = false; } + public string default_adapter_name { get; private set; default = null; } + public string default_adapter_address { get; private set; default = null; } + + private bool _default_adapter_powered = false; + public bool default_adapter_powered { + get { return _default_adapter_powered; } + set { + if (default_adapter == null) return; + if (default_adapter.Powered == value) return; + + var proxy = default_adapter as DBusProxy; + var variant = new Variant.boolean(value); + proxy.call.begin( + "org.freedesktop.DBus.Properties.Set", + new Variant("(ssv)", "org.bluez.Adapter1", "Powered", variant), + DBusCallFlags.NONE, + -1, + null, + adapter_set_powered_cb + ); + } + } + + private bool _default_adapter_setup_mode = false; + public bool default_adapter_setup_mode { + get { return _default_adapter_setup_mode; } + set { set_adapter_discovering(value); } + } + + private Client upower_client; + private bool bluez_devices_coldplugged = false; + private bool has_power_state = true; + + private Queue removed_devices; + private uint removed_devices_id = 0; + + public signal void device_added(BluetoothDevice device); + public signal void device_removed(string path); + + construct { + this.cancellable = new Cancellable(); + this.list_store = new ListStore(typeof(Device1)); + removed_devices = new Queue(); + + // Set up our UPower client + try { + make_upower_client.begin(cancellable, make_upower_cb); + } catch (Error e) { + critical("error creating UPower client: %s", e.message); + return; + } + + // Begin creating our DBus Object Manager for Bluez + try { + this.make_dbus_object_manager.begin(make_client_cb); + } catch (Error e) { + critical("error getting DBusObjectManager for Bluez: %s", e.message); + return; + } + } + + BluetoothClient() { + Object(); + } + + ~BluetoothClient() { + if (cancellable != null) { + cancellable.cancel(); + } + } + + private Type get_proxy_type_func(DBusObjectManagerClient manager, string object_path, string? interface_name) { + if (interface_name == null) { + return typeof(DBusObjectProxy); + } + + if (interface_name == BLUEZ_ADAPTER_INTERFACE) { + return typeof(Adapter1); + } + + if (interface_name == BLUEZ_DEVICE_INTERFACE) { + return typeof(Device1); + } + + return typeof(DBusProxy); + } + + private async Client make_upower_client(Cancellable cancellable) throws Error { + return yield new Client.async(cancellable); + } + + private async DBusObjectManagerClient make_dbus_object_manager() throws Error { + return yield new DBusObjectManagerClient.for_bus( + BusType.SYSTEM, + DBusObjectManagerClientFlags.DO_NOT_AUTO_START, + BLUEZ_DBUS_NAME, + BLUEZ_MANAGER_PATH, + this.get_proxy_type_func, + this.cancellable + ); + } + + private void start_discovery_cb(Object? obj, AsyncResult? res) { + try { + default_adapter.StartDiscovery.end(res); + } catch (Error e) { + var proxy = default_adapter as DBusProxy; + warning("Error calling StartDiscovery() on '%s' org.bluez.Adapter1: %s (%s %d)", proxy.get_object_path(), e.message, e.domain.to_string(), e.code); + discovery_started = false; + } + } + + private void stop_discovery_cb(Object? obj, AsyncResult? res) { + try { + default_adapter.StopDiscovery.end(res); + } catch (Error e) { + var proxy = default_adapter as DBusProxy; + warning("Error calling StopDiscovery() on '%s': %s (%s %d)", proxy.get_object_path(), e.message, e.domain.to_string(), e.code); + discovery_started = false; + } + } + + private void set_discovery_filter_cb(Object? object, AsyncResult? res) { + try { + default_adapter.SetDiscoveryFilter.end(res); + } catch (Error e) { + warning("Error calling SetDiscoveryFilter() on interface org.bluez.Adapter1: %s (%s %d)", e.message, e.domain.to_string(), e.code); + discovery_started = false; + return; + } + + var proxy = default_adapter as DBusProxy; + debug("Starting discovery on %s", proxy.get_object_path()); + default_adapter.StartDiscovery.begin(start_discovery_cb); + } + + private void set_adapter_discovering(bool discovering) { + if (discovery_started) return; + if (default_adapter == null) return; + + var proxy = default_adapter as DBusProxy; + + discovery_started = discovering; + + if (discovering) { + var properties = new HashTable(str_hash, str_equal); + properties["Discoverable"] = discovering; + default_adapter.SetDiscoveryFilter.begin(properties, set_discovery_filter_cb); + } else { + debug("Stopping discovery on %s", proxy.get_object_path()); + default_adapter.StopDiscovery.begin(stop_discovery_cb); + } + } + + /** + * Get the device in our list model with the given path. + * + * If no device is found with the same path, `null` is returned. + */ + private BluetoothDevice? get_device_for_path(string path) { + BluetoothDevice? device = null; + + var num_items = list_store.get_n_items(); + for (int i = 0; i < num_items; i++) { + var d = list_store.get_item(i) as BluetoothDevice; + if (path == d.get_object_path()) { + device = d; + break; + } + } + + return device; + } + + /** + * Get the device in our list model with the given address. + * + * If no device is found with the same address, `null` is returned. + */ + private BluetoothDevice? get_device_for_address(string address) { + BluetoothDevice? device = null; + + var num_items = list_store.get_n_items(); + for (int i = 0; i < num_items; i++) { + var d = list_store.get_item(i) as BluetoothDevice; + if (address == d.address) { + device = d; + break; + } + } + + return device; + } + + /** + * Get the device in our list model with the given UPower device path. + * + * If no device is found with the same path, `null` is returned. + */ + private BluetoothDevice? get_device_for_upower_device(string path) { + BluetoothDevice? device = null; + + var num_items = list_store.get_n_items(); + for (int i = 0; i < num_items; i++) { + var d = list_store.get_item(i) as BluetoothDevice; + var up_device = d.get_upower_device(); + + if (up_device == null) { + continue; + } + if (up_device.get_object_path() == path) { + device = d; + } + } + + return device; + } + + /** + * Tries to get an icon name present in GTK themes for a Bluetooth type. + * + * Not all types have relevant icons. Any type that doesn't have an icon + * will return `null`. + */ + private string? get_icon_for_type(BluetoothType type) { + switch (type) { + case COMPUTER: + return "computer"; + case HEADSET: + return "audio-headset"; + case HEADPHONES: + return "audio-headphones"; + case KEYBOARD: + return "input-keyboard"; + case MOUSE: + return "input-mouse"; + case PRINTER: + return "printer"; + case JOYPAD: + return "input-gaming"; + case TABLET: + return "input-tablet"; + case SPEAKERS: + return "audio-speakers"; + case PHONE: + return "phone"; + case DISPLAY: + return "video-display"; + case SCANNER: + return "scanner"; + default: + return null; + } + } + + /** + * Get the type of a Bluetooth device, and use that type to get an icon for it. + */ + private void get_type_and_icon_for_device(Device1 device, out BluetoothType? type, out string? icon) { + if (type != 0 || icon != null) { + warning("Attempted to get type and icon for device '%s', but type or icon is not 0 or null", device.Name); + return; + } + + // Special case these joypads + if (device.Name == "ION iCade Game Controller" || device.Name == "8Bitdo Zero GamePad") { + type = BluetoothType.JOYPAD; + icon = "input-gaming"; + return; + } + + // First, try to match the appearance of the device + if (type == BluetoothType.ANY) { + type = appearance_to_type(device.Appearance); + } + // Match on the class if the appearance failed + if (type == BluetoothType.ANY) { + type = class_to_type(device.Class); + } + + // Try to get an icon now + icon = get_icon_for_type(type); + + // Fallback to the device's specified icon + if (icon == null) { + icon = device.Icon; + } + + // Fallback to a generic icon + if (icon == null) { + icon = "bluetooth"; + } + } + + /** + * Handle property changes for a Bluetooth device. + */ + private void device_notify_cb(Object obj, ParamSpec pspec) { + Device1 device1 = obj as Device1; + DBusProxy proxy = device1 as DBusProxy; + var property = pspec.name; + + var path = proxy.get_object_path(); + var device = get_device_for_path(path); + + if (device == null) { + debug("Device '%s' not found, ignoring property change for '%s'", path, property); + return; + } + + switch (property) { + case "name": + device.name = device1.Name; + break; + case "alias": + device.alias = device1.Alias; + break; + case "paired": + device.trusted = device1.Trusted; + break; + case "connected": + device.connected = device1.Connected; + break; + case "uuids": + device.uuids = device1.UUIDs; + break; + case "legacy-pairing": + device.legacy_pairing = device1.LegacyPairing; + break; + case "icon": + case "class": + case "appearance": + BluetoothType type = BluetoothType.ANY; + string? icon = null; + + get_type_and_icon_for_device(device1, out type, out icon); + + device.type = type; + device.icon = icon; + default: + debug("Not handling property '%s'", property); + } + } + + private void add_devices_to_list_store() { + var coldplug_upower = !bluez_devices_coldplugged && upower_client != null; + + debug("Emptying device list store since default adapter changed"); + list_store.remove_all(); + + DBusProxy proxy = default_adapter as DBusProxy; + var default_adapter_path = proxy.get_object_path(); + + debug("Coldplugging devices for new default adapter"); + + bluez_devices_coldplugged = true; + var object_list = dbus_object_manager.get_objects(); + + // Add each device from DBus + foreach (var obj in object_list) { + var iface = obj.get_interface(BLUEZ_DEVICE_INTERFACE); + if (iface == null) { + continue; + } + + Device1 device = iface as Device1; + + if (device.Adapter != default_adapter_path) { + continue; + } + + // Connect device 'notify' signal for property changes + device.notify.connect(device_notify_cb); + + // Resolve device type and icon + BluetoothType type = BluetoothType.ANY; + string? icon = null; + get_type_and_icon_for_device(device, out type, out icon); + + debug("Adding device '%s' on adapter '%s' to list store", device.Address, device.Adapter); + + // Create Device object + var device_obj = new BluetoothDevice(device, type, icon); + + // Append to list_store + list_store.append(device_obj); + + // Emit device-added signal + device_added(device_obj); + } + + if (coldplug_upower) { + coldplug_client(); + } + } + + /** + * Get the power state of the current default adapter. + */ + private PowerState get_state() { + if (default_adapter == null) { + return PowerState.ABSENT; + } + + var state = default_adapter.PoweredState; + + // Check if we have a valid power state + if (state == null) { + has_power_state = false; + + // Fallback to either on or off + return default_adapter.Powered ? PowerState.ON : PowerState.OFF; + } + + return PowerState.from_string(state); + } + + private bool is_default_adapter(Adapter1? adapter) { + if (this.default_adapter == null) { + return false; + } + + if (adapter == null) { + return false; + } + + DBusProxy adapter_proxy = adapter as DBusProxy; + DBusProxy default_proxy = default_adapter as DBusProxy; + + return (adapter_proxy.get_object_path() == default_proxy.get_object_path()); + } + + private bool should_be_default_adapter(Adapter1 adapter) { + DBusProxy proxy = adapter as DBusProxy; + DBusProxy default_proxy = this.default_adapter as DBusProxy; + + return proxy.get_object_path() == default_proxy.get_object_path(); + } + + /** + * Reset the default_adapter properties to their defaults. + */ + private void reset_default_adapter_props() { + default_adapter = null; + default_adapter_address = null; + default_adapter_powered = false; + default_adapter_state = PowerState.ABSENT; + discovery_started = false; + default_adapter_name = null; + } + + /** + * Updates the default_adapter_* properties from the current default adapter. + */ + private void update_default_adapter_props() { + default_adapter_address = default_adapter.Address; + default_adapter_powered = default_adapter.Powered; + default_adapter_state = PowerState.from_string(default_adapter.PoweredState); + discovery_started = default_adapter.Discovering; + default_adapter_name = default_adapter.Name; + } + + /** + * Handles when the default Bluetooth adapter changes. + */ + private void default_adapter_changed(DBusProxy proxy, AdapterChangeType change_type) { + Adapter1 adapter = proxy as Adapter1; + + switch (change_type) { + case REMOVED: + reset_default_adapter_props(); + list_store.remove_all(); + return; + case REPLACEMENT: + list_store.remove_all(); + set_adapter_discovering(false); + default_adapter = null; + break; + default: // Handles new default and owner update cases + default_adapter = null; + break; + } + + default_adapter = adapter; + adapter.notify.connect(adapter_notify_cb); + + // Bail if the change was only an update + if (change_type == OWNER_UPDATE) { + return; + } + + add_devices_to_list_store(); + update_default_adapter_props(); + } + + private void adapter_set_powered_cb(Object? obj, AsyncResult? res) { + var proxy = default_adapter as DBusProxy; + + try { + proxy.call.end(res); + } catch (Error e) { + warning("Error setting property 'Powered' on %s: %s (%s, %d)", proxy.get_object_path(), e.message, e.domain.to_string(), e.code); + } + } + + /** + * Process the device removal queue, removing all devices with paths + * in the queue from our list store. + */ + private bool unqueue_device_removal() { + if (removed_devices == null || removed_devices.is_empty()) return Source.REMOVE; + + // Iterate over the queue + string? path = null; + while ((path = removed_devices.pop_head()) != null) { + var found = false; + var num_items = list_store.get_n_items(); + + debug("Processing '%s' in removal queue", path); + + // Iterate over our list store to try to find the correct device + for (var i = 0; i < num_items; i++) { + var device = list_store.get_item(i) as BluetoothDevice; + + // Check if the path for this device matches the current queue item + if (path != device.get_object_path()) continue; + + // Matching device was found, remove it + device_removed(path); + list_store.remove(i); + found = true; + break; + } + + if (!found) debug("Device %s not known, ignoring", path); + } + + // Clear any remaining devices from the queue + removed_devices.clear(); + return Source.REMOVE; + } + + /** + * Adds a new Bluetooth device to our list store, or updates an + * existing one if it already exists. + */ + private void add_device(Device1 device) { + var adapter_path = device.Adapter; + var default_adapter_proxy = default_adapter as DBusProxy; + var default_adapter_path = default_adapter_proxy.get_object_path(); + + // Ensure that the device is on the current default adapter + if (adapter_path != default_adapter_path) return; + + device.notify.connect(device_notify_cb); + + var device_proxy = device as DBusProxy; + var device_path = device_proxy.get_object_path(); + var device_object = get_device_for_path(device_path); + + // Update the device if it's already been added + if (device_object != null) { + debug("Updating proxy for device '%s'", device_path); + device_object.proxy = device_proxy; + return; + } + + BluetoothType type = 0; + string? icon = null; + get_type_and_icon_for_device(device, out type, out icon); + + debug("Adding device '%s' to adapter '%s'", device.Address, adapter_path); + + device_object = new BluetoothDevice(device, type, icon); + list_store.append(device_object); + device_added(device_object); + } + + /** + * Adds a device to the queue for removal. + */ + private void queue_remove_device(string path) { + debug("Queueing removal of device %s", path); + removed_devices.push_head(path); + + // Remove the current task to process the queue, if any + if (removed_devices_id != 0) { + Source.remove(removed_devices_id); + } + + // Add a task to process the queue + removed_devices_id = Timeout.add(DEVICE_REMOVAL_TIMEOUT, unqueue_device_removal); + } + + /** + * Handles property changes on a Bluetooth adapter. + * + * If the adapter is not the current default adapter, then + * nothing is updated. + */ + private void adapter_notify_cb(Object obj, ParamSpec pspec) { + Adapter1 adapter = obj as Adapter1; + DBusProxy proxy = adapter as DBusProxy; + + var property = pspec.name; + var adapter_path = proxy.get_object_path(); + + if (default_adapter == null) { + debug("Property '%s' changed on adapter '%s', but default adapter not set yet", property, adapter_path); + return; + } + + if (adapter != default_adapter) { + debug("Ignoring property change '%s' change on non-default adapter '%s'", property, adapter_path); + return; + } + + debug("Property change received for adapter '%s': %s", adapter_path, property); + + // Update the client property that changed on the adapter + switch (property) { + case "alias": + default_adapter_name = adapter.Alias; + case "discovering": + discovery_started = adapter.Discovering; + case "powered": + default_adapter_powered = adapter.Powered; + if (!has_power_state) { + default_adapter_state = get_state(); + } + case "power-state": + default_adapter_state = get_state(); + } + } + + private void add_adapter(Adapter1 adapter) { + DBusProxy proxy = adapter as DBusProxy; + + var name = proxy.get_name_owner(); + var iface = proxy.get_interface_name(); + var path = proxy.get_object_path(); + + if (this.default_adapter == null) { + debug("Adding adapter %s %s %s", name, path, iface); + default_adapter_changed(proxy, NEW_DEFAULT); + } else if (is_default_adapter(adapter)) { + debug("Updating default adapter with new proxy %s %s %s", name, path, iface); + default_adapter_changed(proxy, OWNER_UPDATE); + } else if (should_be_default_adapter(adapter)) { + var default_proxy = default_adapter as DBusProxy; + debug("Replacing adapter %s with %s %s %s", default_proxy.get_name_owner(), name, path, iface); + default_adapter_changed(proxy, REPLACEMENT); + } else { + debug("Ignoring non-default adapter %s %s %s", name, path, iface); + return; + } + + this.num_adapters++; + } + + private void adapter_removed(string path) { + DBusProxy default_proxy = this.default_adapter as DBusProxy; + DBusProxy new_default_adapter = null; + bool was_default = false; + + // Check if this is the path to the current default adapter + if (strcmp(path, default_proxy.get_object_path()) == 0) { + was_default = true; + } + + if (was_default) { + this.num_adapters--; + return; + } + + // Look through the list of DBus objects for a new default adapter + var object_list = this.dbus_object_manager.get_objects(); + foreach (var object in object_list) { + var iface = object.get_interface(BLUEZ_ADAPTER_INTERFACE); + if (iface != null) { + new_default_adapter = iface as DBusProxy; + break; + } + } + + // Decide if we have a removal, or if we have a new default + var change_type = new_default_adapter == null ? AdapterChangeType.REMOVED : AdapterChangeType.NEW_DEFAULT; + + // Handle a removal + if (change_type == REMOVED) { + // TODO: Clear the removed_devices queue + } + + default_adapter_changed(new_default_adapter, change_type); + this.num_adapters--; + } + + private void interface_added(DBusObject object, DBusInterface iface) { + if (iface.get_type() == typeof(Adapter1)) { + Adapter1 adapter = iface as Adapter1; + add_adapter(adapter); + } else if (iface.get_type() == typeof(Device1)) { + Device1 device = iface as Device1; + add_device(device); + } + } + + private void interface_removed(DBusObject object, DBusInterface iface) { + if (iface.get_type() == typeof(Adapter1)) { + adapter_removed(object.get_object_path()); + } else if (iface.get_type() == typeof(Device1)) { + queue_remove_device(object.get_object_path()); + } + } + + private void object_added(DBusObject object) { + var ifaces = object.get_interfaces(); + foreach (var iface in ifaces) { + interface_added(object, iface); + } + } + + private void object_removed(DBusObject object) { + var ifaces = object.get_interfaces(); + foreach (var iface in ifaces) { + interface_removed(object, iface); + } + } + + private List? filter_adapter_list(List object_list) { + List ret = null; + + foreach (var object in object_list) { + var iface = object.get_interface(BLUEZ_ADAPTER_INTERFACE); + if (iface != null) ret.append(iface); + } + + return ret; + } + + private void make_client_cb(Object? obj, AsyncResult? res) { + try { + dbus_object_manager = make_dbus_object_manager.end(res); + } catch (Error e) { + if (!e.matches(DBusError.IO_ERROR, IOError.CANCELLED)) { + critical("error getting DBusObjectManager for Bluez: %s", e.message); + } + return; + } + + // Connect manager signals + dbus_object_manager.interface_added.connect(interface_added); + dbus_object_manager.interface_removed.connect(interface_removed); + + dbus_object_manager.object_added.connect(object_added); + dbus_object_manager.object_removed.connect(object_removed); + + // Create the adapter list + var object_list = dbus_object_manager.get_objects(); + var adapter_list = filter_adapter_list(object_list); + + // Reverse sort the adapter list + adapter_list.sort((a, b) => { + DBusProxy adapter_a = a as DBusProxy; + DBusProxy adapter_b = b as DBusProxy; + + return adapter_b.get_object_path().collate(adapter_a.get_object_path()); + }); + + // Add all of the adapters + debug("Adding adapters from DBus Object Manager"); + foreach (var adapter in adapter_list) { + add_adapter(adapter as Adapter1); + } + } + + /** + * Handle when a UPower device is being added. + */ + private void upower_device_added_cb(Device up_device) { + var serial = up_device.serial; + + // Make sure the device has a valid Bluetooth address + if (serial == null || !is_valid_address(serial)) { + return; + } + + var device = get_device_for_address(serial); + + if (device == null) { + warning("Could not find Bluetooth device for UPower device with serial '%s'", serial); + return; + } + + // Connect signals + up_device.notify["battery-level"].connect(() => device.update_battery(up_device)); + up_device.notify["percentage"].connect(() => device.update_battery(up_device)); + + // Update the power properties + device.set_upower_device(up_device); + device.update_battery(up_device); + } + + /** + * Handles the removal of a UPower device. + * + * The Bluetooth device corresponding to the UPower device will have its + * association removed, and its battery properties reset. + */ + private void upower_device_removed_cb(string object_path) { + var device = get_device_for_upower_device(object_path); + + if (device == null) { + return; + } + + debug("Removing Upower Device '%s' for Bluetooth device '%s'", object_path, device.get_object_path()); + + // Reset device power properties + device.set_upower_device(null); + device.battery_type = BatteryType.NONE; + device.battery_level = DeviceLevel.NONE; + device.battery_percentage = 0.0f; + } + + /** + * Gets the result of the asynchronous UPower get_devices call and + * calls our device_added function to try to map them to Bluetooth + * devices. + */ + private void upower_get_devices_cb(Object? obj, AsyncResult? res) { + GenericArray devices = null; + + try { + devices = upower_client.get_devices_async.end(res); + } catch (Error e) { + warning("Error getting UPower devices: %s", e.message); + return; + } + + if (devices == null) { + warning("No UPower devices found"); + return; + } + + debug("Found %d UPower devices", devices.length); + + // Add each UPower device + foreach (var device in devices) { + upower_device_added_cb(device); + } + } + + /** + * Gets all UPower devices for the current Upower client, and tries to associate + * each UPower device with the corresponding Bluetooth device. + */ + private void coldplug_client() { + if (upower_client == null) { + return; + } + + // Get the UPower devices asynchronously + upower_client.get_devices_async.begin(cancellable, upower_get_devices_cb); + } + + private void make_upower_cb(Object? obj, AsyncResult? res) { + try { + upower_client = make_upower_client.end(res); + } catch (Error e) { + critical("Error creating UPower client: %s", e.message); + return; + } + + upower_client.device_added.connect(upower_device_added_cb); + upower_client.device_removed.connect(upower_device_removed_cb); + + // Maybe coldplug UPower devices + if (bluez_devices_coldplugged) { + coldplug_client(); + } + } + + /** + * Gets the type of Bluetooth device based on its appearance value. + * This is usually found in the GAP service. + */ + private BluetoothType appearance_to_type(uint16 appearance) { + switch ((appearance & 0xffc0) >> 6) { + case 0x01: + return PHONE; + case 0x02: + return COMPUTER; + case 0x05: + return DISPLAY; + case 0x0a: + return OTHER_AUDIO; + case 0x0b: + return SCANNER; + case 0x0f: /* HID Generic */ + switch (appearance & 0x3f) { + case 0x01: + return KEYBOARD; + case 0x02: + return MOUSE; + case 0x03: + case 0x04: + return JOYPAD; + case 0x05: + return TABLET; + case 0x08: + return SCANNER; + } + break; + case 0x21: + return SPEAKERS; + case 0x25: /* Audio */ + switch (appearance & 0x3f) { + case 0x01: + case 0x02: + case 0x04: + return HEADSET; + case 0x03: + return HEADPHONES; + default: + return OTHER_AUDIO; + } + break; + } + + return ANY; + } + + /** + * Gets the type of a Bluetooth device based on its class. + */ + private BluetoothType class_to_type(uint32 klass) { + switch ((klass & 0x1f00) >> 8) { + case 0x01: + return COMPUTER; + case 0x02: + switch ((klass & 0xfc) >> 2) { + case 0x01: + case 0x02: + case 0x03: + case 0x05: + return PHONE; + case 0x04: + return MODEM; + } + break; + case 0x03: + return NETWORK; + case 0x04: + switch ((klass & 0xfc) >> 2) { + case 0x01: + case 0x02: + return HEADSET; + case 0x05: + return SPEAKERS; + case 0x06: + return HEADPHONES; + case 0x0b: /* VCR */ + case 0x0c: /* Video Camera */ + case 0x0d: /* Camcorder */ + return VIDEO; + default: + return OTHER_AUDIO; + } + break; + case 0x05: + switch ((klass & 0xc0) >> 6) { + case 0x00: + switch ((klass & 0x1e) >> 2) { + case 0x01: + case 0x02: + return JOYPAD; + case 0x03: + return REMOTE_CONTROL; + } + break; + case 0x01: + return KEYBOARD; + case 0x02: + switch ((klass & 0x1e) >> 2) { + case 0x05: + return TABLET; + default: + return MOUSE; + } + } + break; + case 0x06: + if ((klass & 0x80) == 1) + return PRINTER; + if ((klass & 0x40) == 1) + return SCANNER; + if ((klass & 0x20) == 1) + return CAMERA; + if ((klass & 0x10) == 1) + return DISPLAY; + break; + case 0x07: + return WEARABLE; + case 0x08: + return TOY; + } + + return ANY; + } + + /** + * Check if a Bluetooth address is valid. + */ + private bool is_valid_address(string address) { + if (address.length != 17) { + return false; + } + + for (var i = 0; i < 17; i++) { + if (((i + 1) % 3) == 0) { + if (address[i] != ':') { + return false; + } + continue; + } + + if (!address[i].isxdigit()) { + return false; + } + } + + return true; + } +} diff --git a/src/panel/applets/status/BluetoothDBus.vala b/src/panel/applets/status/BluetoothDBus.vala new file mode 100644 index 000000000..5892cb39c --- /dev/null +++ b/src/panel/applets/status/BluetoothDBus.vala @@ -0,0 +1,75 @@ +/* + * This file is part of budgie-desktop + * + * Copyright © 2015-2022 Budgie Desktop Developers + * Copyright (C) 2015 Alberts Muktupāvels + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +/** + * Definition for Bluez Adapter1 interface. + */ +[DBus (name="org.bluez.Adapter1")] +public interface Adapter1 : GLib.Object { + public abstract string Address { get; } + public abstract string Name { get; } + public abstract string Alias { get; set; } + public abstract uint32 Class { get; } + public abstract bool Powered { get; set; } + public abstract string PoweredState { get; } + public abstract bool Discoverable { set; get; } + public abstract uint32 DiscoverableTimeout { get; set; } + public abstract bool Pairable { get; set; } + public abstract uint32 PairableTimeout { get; set; } + public abstract bool Discovering { get; set; } + public abstract string[] UUIDS { get; } + public abstract string Modalias { get; } + + public async abstract void StartDiscovery() throws GLib.DBusError, GLib.IOError; + public async abstract void StopDiscovery() throws GLib.DBusError, GLib.IOError; + public async abstract void RemoveDevice(GLib.ObjectPath device) throws GLib.DBusError, GLib.IOError; + public async abstract void SetDiscoveryFilter(HashTable properties) throws GLib.DBusError, GLib.IOError; +} + +/** + * Definition of the Bluez Device1 interface. + */ +[DBus (name = "org.bluez.Device1")] +public interface Device1 : GLib.Object { + public abstract string Address { get; } + public abstract string Name { get; } + public abstract string Alias { get; set; } + public abstract uint32 Class { get; } + public abstract uint16 Appearance { get; } + public abstract string Icon { get; } + public abstract bool Paired { get; } + public abstract bool Trusted { get; set; } + public abstract bool Blocked { get; set; } + public abstract bool LegacyPairing { get; } + public abstract int16 RSSI { get; } + public abstract bool Connected { get; } + public abstract string[] UUIDs { get; } + public abstract string Modalias { get; } + public abstract GLib.ObjectPath Adapter { get; } + + public async abstract void Connect() throws GLib.DBusError, GLib.IOError; + public async abstract void Disconnect() throws GLib.DBusError, GLib.IOError; + public async abstract void ConnectProfile(string uuid) throws GLib.DBusError, GLib.IOError; + public async abstract void DisconnectProfile(string uuid) throws GLib.DBusError, GLib.IOError; + public async abstract void Pair() throws GLib.DBusError, GLib.IOError; + public async abstract void CancelPairing() throws GLib.DBusError, GLib.IOError; +} + +/** + * Definition of the Bluez AgentManager1 interface. + */ +[DBus (name = "org.bluez.AgentManager1")] +public interface AgentManager1 : GLib.Object { + public async abstract void RegisterAgent(GLib.ObjectPath agent, string capability) throws GLib.DBusError, GLib.IOError; + public async abstract void UnregisterAgent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; + public async abstract void RequestDefaultAgent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; +} diff --git a/src/panel/applets/status/BluetoothDevice.vala b/src/panel/applets/status/BluetoothDevice.vala new file mode 100644 index 000000000..cc366f05a --- /dev/null +++ b/src/panel/applets/status/BluetoothDevice.vala @@ -0,0 +1,96 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +using GLib; +using Up; + +/** + * Wrapper for a Bluetooth device. + */ +public class BluetoothDevice : Object { + public DBusProxy proxy { get; set; default = null; } + public string address { get; set; } + public string alias { get; set; } + public string name { get; set; } + public BluetoothType type { get; set; default = BluetoothType.ANY; } + public string icon { get; set; } + public bool paired { get; set; default = false; } + public bool trusted { get; set; default = false; } + public bool connected { get; set; default = false; } + public bool legacy_pairing { get; set; default = false; } + public string[] uuids { get; set; } + public bool connectable { get; set; default = false; } + public BatteryType battery_type { get; set; default = BatteryType.NONE; } + [IntegerType (min = 0, max = 100)] + public double battery_percentage { get; set; default = 0.0; } + public DeviceLevel battery_level { get; set; default = DeviceLevel.UNKNOWN; } + + /** + * Create a new Bluetooth device wrapper object. + */ + public BluetoothDevice(Device1 device, BluetoothType type, string icon) { + Object( + proxy: device as DBusProxy, + address: device.Address, + alias: device.Alias, + name: device.Name, + type: type, + icon: icon, + legacy_pairing: device.LegacyPairing, + uuids: device.UUIDs, + paired: device.Paired, + connected: device.Connected, + trusted: device.Trusted + ); + } + + /** + * Gets the object path for this Bluetooth device. + */ + public string? get_object_path() { + if (proxy == null) { + return null; + } + + return proxy.get_object_path(); + } + + /** + * Get the associated UPower device for this Bluetooth device. + */ + public Device get_upower_device() { + return get_data("up-device"); + } + + /** + * Set an association between this Bluetooth device and a UPower device. + */ + public void set_upower_device(Device? up_device) { + set_data_full("up-device", up_device != null ? up_device.ref() : null, unref); + } + + /** + * Updates battery levels from a UPower device. + */ + public void update_battery(Device up_device) { + BatteryType type; + + if (up_device.battery_level == DeviceLevel.NONE) { + type = BatteryType.PERCENTAGE; + } else { + type = BatteryType.COARSE; + } + + battery_type = type; + battery_level = up_device.battery_level as DeviceLevel; + battery_percentage = up_device.percentage; + } +} diff --git a/src/panel/applets/status/BluetoothEnums.vala b/src/panel/applets/status/BluetoothEnums.vala new file mode 100644 index 000000000..1d0b34093 --- /dev/null +++ b/src/panel/applets/status/BluetoothEnums.vala @@ -0,0 +1,84 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Inspired by gnome-bluetooth. + */ + +/** + * The type of battery reporting supported by the device. + */ +public enum BatteryType { + /** No battery reporting. */ + NONE, + /** Battery level reported in percentage. */ + PERCENTAGE, + /** Battery level reported coarsely. */ + COARSE +} + +/** + * The type of a Bluetooth device. + */ +[Flags] +public enum BluetoothType { + ANY = 1 << 0, + PHONE = 1 << 1, + MODEM = 1 << 2, + COMPUTER = 1 << 3, + NETWORK = 1 << 4, + HEADSET = 1 << 5, + HEADPHONES = 1 << 6, + OTHER_AUDIO = 1 << 7, + KEYBOARD = 1 << 8, + MOUSE = 1 << 9, + CAMERA = 1 << 10, + PRINTER = 1 << 11, + JOYPAD = 1 << 12, + TABLET = 1 << 13, + VIDEO = 1 << 14, + REMOTE_CONTROL = 1 << 15, + SCANNER = 1 << 16, + DISPLAY = 1 << 17, + WEARABLE = 1 << 18, + TOY = 1 << 19, + SPEAKERS = 1 << 20 +} + +/** + * A more precise power state for a Bluetooth adapter. + */ +public enum PowerState { + /** Bluetooth adapter is missing. */ + ABSENT = 0, + /** Bluetooth adapter is on. */ + ON, + /** Bluetooth adapter is being turned on. */ + TURNING_ON, + /** Bluetooth adapter is being turned off. */ + TURNING_OFF, + /** Bluetooth adapter is off. */ + OFF; + + /** + * Try to match a string to a PowerState. + * + * If no match is found, returns PowerState.ABSENT. + */ + public static PowerState from_string(string state) { + switch (state) { + case "on": return ON; + case "off-enabling": return TURNING_ON; + case "on-disabling": return TURNING_OFF; + case "off": + case "off-blocked": return OFF; + default: return ABSENT; + } + } +} diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index b54bbf6e0..00ec9a97e 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -12,11 +12,6 @@ * BluetoothIndicator is largely inspired by gnome-flashback. */ -[DBus (name="org.gnome.SettingsDaemon.Rfkill")] -public interface Rfkill : GLib.Object { - public abstract bool BluetoothAirplaneMode { set; get; } -} - public class BluetoothIndicator : Gtk.Bin { public Gtk.Image? image = null; @@ -27,8 +22,6 @@ public class BluetoothIndicator : Gtk.Bin { private Gtk.TreeModel? model = null; public Budgie.Popover? popover = null; - Rfkill? killer = null; - DBusProxy? db = null; Gtk.CheckButton radio_airplane; ulong radio_id; @@ -62,7 +55,7 @@ public class BluetoothIndicator : Gtk.Bin { send_to = new Gtk.Button.with_label(_("Send Files")); send_to.get_child().set_halign(Gtk.Align.START); send_to.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); - send_to.clicked.connect(on_send_file); + // send_to.clicked.connect(on_send_file); box.pack_start(send_to, false, false, 0); var sep = new Gtk.Separator(Gtk.Orientation.HORIZONTAL); @@ -71,7 +64,6 @@ public class BluetoothIndicator : Gtk.Bin { // Airplane mode radio_airplane = new Gtk.CheckButton.with_label(_("Bluetooth Airplane Mode")); radio_airplane.get_child().set_property("margin", 4); - radio_id = radio_airplane.notify["active"].connect_after(on_set_airplane); box.pack_start(radio_airplane, false, false, 0); // Ensure all content is shown @@ -88,10 +80,7 @@ public class BluetoothIndicator : Gtk.Bin { this.resync(); this.setup_dbus.begin(() => { - if (this.killer == null) { - return; - } - this.sync_rfkill(); + }); show_all(); @@ -99,9 +88,7 @@ public class BluetoothIndicator : Gtk.Bin { private bool on_button_release_event(Gdk.EventButton e) { if (e.button == Gdk.BUTTON_MIDDLE) { // Middle click - if (killer != null) { - killer.BluetoothAirplaneMode = !killer.BluetoothAirplaneMode; // Invert our current bluetooth airplane mode - } + } else { return Gdk.EVENT_PROPAGATE; } @@ -111,10 +98,7 @@ public class BluetoothIndicator : Gtk.Bin { async void setup_dbus() { try { - killer = yield Bus.get_proxy(BusType.SESSION, "org.gnome.SettingsDaemon.Rfkill", "/org/gnome/SettingsDaemon/Rfkill"); } catch (Error e) { - killer = null; - warning("Unable to contact RfKill manager: %s", e.message); return; } } @@ -215,50 +199,4 @@ public class BluetoothIndicator : Gtk.Bin { message("Unable to launch budgie-bluetooth-panel.desktop: %s", e.message); } } - - void on_send_file() { - this.popover.hide(); - - try { - var app_info = AppInfo.create_from_commandline("bluetooth-sendto", "Bluetooth Transfer", AppInfoCreateFlags.NONE); - if (app_info == null) { - return; - } - - try { - app_info.launch(null, null); - } catch (Error e) { - message("Unable to launch bluetooth-sendto: %s", e.message); - } - } catch (Error e) { - message("Unable to create bluetooth-sendto AppInfo: %s", e.message); - } - } - - /* We set */ - void on_set_airplane() { - bool s = radio_airplane.get_active(); - - try { - killer.BluetoothAirplaneMode = s; - } catch (Error e) { - message("Error setting airplane mode: %s", e.message); - } - this.popover.hide(); - } - - /* Notify */ - void on_airplane_change() { - SignalHandler.block(radio_airplane, radio_id); - radio_airplane.set_active(killer.BluetoothAirplaneMode); - SignalHandler.unblock(radio_airplane, radio_id); - this.resync(); - } - - void sync_rfkill() { - db = killer as DBusProxy; - db.g_properties_changed.connect(on_airplane_change); - this.resync(); - this.on_airplane_change(); - } } diff --git a/src/panel/applets/status/meson.build b/src/panel/applets/status/meson.build index 3ace699e4..c1a2b28f0 100644 --- a/src/panel/applets/status/meson.build +++ b/src/panel/applets/status/meson.build @@ -19,6 +19,10 @@ applet_status_resources = gnome.compile_resources( ) applet_status_sources = [ + 'BluetoothClient.vala', + 'BluetoothDBus.vala', + 'BluetoothDevice.vala', + 'BluetoothEnums.vala', 'BluetoothIndicator.vala', 'StatusApplet.vala', 'PowerIndicator.vala', @@ -32,19 +36,20 @@ applet_status_deps = [ dep_gtk3, dep_peas, dep_accountsservice, - dependency('upower-glib', version: '>= 0.99.0'), link_libpanelplugin, + dependency('upower-glib', version: '>= 0.99.14'), gvc.get_variable('libgvc_dep'), meson.get_compiler('c').find_library('m', required: false), ] +# TODO: Nuke if with_bluetooth == true applet_status_deps += dependency('gnome-bluetooth-1.0', version: '>= 3.34.0') endif shared_library( 'statusapplet', - applet_status_sources, + sources: applet_status_sources, dependencies: applet_status_deps, c_args: [ '-lm' diff --git a/vapi/README.md b/vapi/README.md index 77bc18058..6bd29f17d 100644 --- a/vapi/README.md +++ b/vapi/README.md @@ -8,6 +8,10 @@ To refresh the Polkit vapi files: Then have fun un-mangling it to support vala async syntax +To generate the UPower vapi files: + + vapigen --library upower-glib /usr/share/gir-1.0/UpowerGlib-1.0.gir --metadatadir . --pkg gio-unix-2.0 UPowerGlib-1.0-custom.vala + For mutter (and shipped cogl and clutter), once you defined the relative `*.deps`, `*.metadata` and `*-custom.vala` files, you can run: ./vapi/generate-mutter-vapi.sh diff --git a/vapi/UPowerGlib-1.0-custom.vala b/vapi/UPowerGlib-1.0-custom.vala new file mode 100644 index 000000000..7ea3340b4 --- /dev/null +++ b/vapi/UPowerGlib-1.0-custom.vala @@ -0,0 +1,5 @@ +public class Up.Client : GLib.Object { + [CCode (cname = "up_client_new_async", has_construct_function = false)] + [Version (since = "0.99.14")] + public async Client.@async (GLib.Cancellable? cancellable = null) throws GLib.Error; +} diff --git a/vapi/UPowerGlib-1.0.metadata b/vapi/UPowerGlib-1.0.metadata new file mode 100644 index 000000000..38c7b6976 --- /dev/null +++ b/vapi/UPowerGlib-1.0.metadata @@ -0,0 +1,5 @@ +Client + .new_async skip=true + .new_finish skip=true + +*.*.cancellable default=null diff --git a/vapi/upower-glib.vapi b/vapi/upower-glib.vapi index 531c76fd8..70e3dd037 100644 --- a/vapi/upower-glib.vapi +++ b/vapi/upower-glib.vapi @@ -1,314 +1,266 @@ -/* upower-glib-1.0.vapi generated by vapigen, do not modify. */ +/* upower-glib.vapi generated by vapigen, do not modify. */ [CCode (cprefix = "Up", gir_namespace = "UPowerGlib", gir_version = "1.0", lower_case_cprefix = "up__")] namespace Up { [CCode (cheader_filename = "upower.h", type_id = "up_client_get_type ()")] - public class Client : GLib.Object { + public class Client : GLib.Object, GLib.AsyncInitable, GLib.Initable { [CCode (cname = "up_client_new", has_construct_function = false)] + [Version (since = "0.9.0")] public Client (); - [CCode (cname = "up_client_about_to_sleep_sync")] - public bool about_to_sleep_sync (Up.SleepKind sleep_kind, GLib.Cancellable? cancellable = null) throws GLib.Error; - [CCode (cname = "up_client_enumerate_devices_sync")] - public bool enumerate_devices_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; - [CCode (cname = "up_client_get_can_hibernate")] - public bool get_can_hibernate (); - [CCode (cname = "up_client_get_can_suspend")] - public bool get_can_suspend (); + [CCode (cname = "up_client_new_async", has_construct_function = false)] + [Version (since = "0.99.14")] + public async Client.@async (GLib.Cancellable? cancellable = null) throws GLib.Error; + [CCode (cname = "up_client_new_full", has_construct_function = false)] + [Version (since = "0.99.5")] + public Client.full (GLib.Cancellable? cancellable = null) throws GLib.Error; + [CCode (cname = "up_client_get_critical_action")] + [Version (since = "1.0")] + public string get_critical_action (); [CCode (cname = "up_client_get_daemon_version")] + [Version (since = "0.9.0")] public unowned string get_daemon_version (); [CCode (cname = "up_client_get_devices")] + [Version (deprecated = true, deprecated_since = "0.99.8", since = "0.9.0")] public GLib.GenericArray get_devices (); - [CCode (cname = "up_client_get_is_docked")] - public bool get_is_docked (); - [CCode (cname = "up_client_get_lid_force_sleep")] - public bool get_lid_force_sleep (); + [CCode (cname = "up_client_get_devices2")] + [Version (since = "0.99.8")] + public GLib.GenericArray get_devices2 (); + [CCode (cname = "up_client_get_devices_async")] + [Version (since = "0.99.14")] + public async GLib.GenericArray get_devices_async (GLib.Cancellable? cancellable = null) throws GLib.Error; + [CCode (cname = "up_client_get_display_device")] + [Version (since = "1.0")] + public Up.Device get_display_device (); [CCode (cname = "up_client_get_lid_is_closed")] + [Version (since = "0.9.0")] public bool get_lid_is_closed (); [CCode (cname = "up_client_get_lid_is_present")] + [Version (since = "0.9.2")] public bool get_lid_is_present (); [CCode (cname = "up_client_get_on_battery")] + [Version (since = "0.9.0")] public bool get_on_battery (); - [CCode (cname = "up_client_get_on_low_battery")] - public bool get_on_low_battery (); - [CCode (cname = "up_client_get_properties_sync")] - public bool get_properties_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; - [CCode (cname = "up_client_hibernate_sync")] - public bool hibernate_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; - [CCode (cname = "up_client_suspend_sync")] - public bool suspend_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; - [NoAccessorMethod] - public bool can_hibernate { get; } - [NoAccessorMethod] - public bool can_suspend { get; } [NoAccessorMethod] + [Version (since = "0.9.0")] public string daemon_version { owned get; } [NoAccessorMethod] - public bool is_docked { get; } - [NoAccessorMethod] - public bool lid_force_sleep { get; } - [NoAccessorMethod] + [Version (since = "0.9.0")] public bool lid_is_closed { get; } [NoAccessorMethod] + [Version (since = "0.9.0")] public bool lid_is_present { get; } [NoAccessorMethod] + [Version (since = "0.9.0")] public bool on_battery { get; } - [NoAccessorMethod] - public bool on_low_battery { get; } - public virtual signal void changed (); + [Version (since = "0.9.0")] public virtual signal void device_added (Up.Device device); + [Version (since = "1.0")] public virtual signal void device_removed (string object_path); - public virtual signal void notify_resume (uint sleep_kind); - public virtual signal void notify_sleep (uint sleep_kind); } [CCode (cheader_filename = "upower.h", type_id = "up_device_get_type ()")] public class Device : GLib.Object { [CCode (cname = "up_device_new", has_construct_function = false)] + [Version (since = "0.9.0")] public Device (); [CCode (cname = "up_device_get_history_sync")] + [Version (since = "0.9.0")] public GLib.GenericArray get_history_sync (string type, uint timespec, uint resolution, GLib.Cancellable? cancellable = null) throws GLib.Error; [CCode (cname = "up_device_get_object_path")] + [Version (since = "0.9.0")] public unowned string get_object_path (); [CCode (cname = "up_device_get_statistics_sync")] + [Version (since = "0.9.0")] public GLib.GenericArray get_statistics_sync (string type, GLib.Cancellable? cancellable = null) throws GLib.Error; [CCode (cname = "up_device_kind_from_string")] + [Version (since = "0.9.0")] public static Up.DeviceKind kind_from_string (string type); [CCode (cname = "up_device_kind_to_string")] + [Version (since = "0.9.0")] public static unowned string kind_to_string (Up.DeviceKind type_enum); + [CCode (cname = "up_device_level_from_string")] + [Version (since = "1.0")] + public static Up.DeviceLevel level_from_string (string level); + [CCode (cname = "up_device_level_to_string")] + [Version (since = "1.0")] + public static unowned string level_to_string (Up.DeviceLevel level_enum); [CCode (cname = "up_device_refresh_sync")] + [Version (since = "0.9.0")] public bool refresh_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; [CCode (cname = "up_device_set_object_path_sync")] + [Version (since = "0.9.0")] public bool set_object_path_sync (string object_path, GLib.Cancellable? cancellable = null) throws GLib.Error; [CCode (cname = "up_device_state_from_string")] + [Version (since = "0.9.0")] public static Up.DeviceState state_from_string (string state); [CCode (cname = "up_device_state_to_string")] + [Version (since = "0.9.0")] public static unowned string state_to_string (Up.DeviceState state_enum); [CCode (cname = "up_device_technology_from_string")] + [Version (since = "0.9.0")] public static Up.DeviceTechnology technology_from_string (string technology); [CCode (cname = "up_device_technology_to_string")] + [Version (since = "0.9.0")] public static unowned string technology_to_string (Up.DeviceTechnology technology_enum); [CCode (cname = "up_device_to_text")] + [Version (since = "0.9.0")] public string to_text (); [NoAccessorMethod] + [Version (since = "1.0")] + public uint battery_level { get; set; } + [NoAccessorMethod] + [Version (since = "0.9.0")] public double capacity { get; set; } [NoAccessorMethod] + [Version (since = "1.0")] + public int charge_cycles { get; set; } + [NoAccessorMethod] + [Version (since = "0.9.0")] public double energy { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double energy_empty { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double energy_full { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double energy_full_design { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double energy_rate { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public bool has_history { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public bool has_statistics { get; set; } [NoAccessorMethod] + [Version (since = "1.0")] + public string icon_name { owned get; set; } + [NoAccessorMethod] + [Version (since = "0.9.0")] public bool is_present { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public bool is_rechargeable { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public uint kind { get; set; } [NoAccessorMethod] + [Version (since = "0.9.19")] public double luminosity { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public string model { owned get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public string native_path { owned get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public bool online { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double percentage { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public bool power_supply { get; set; } [NoAccessorMethod] - public bool recall_notice { get; set; } - [NoAccessorMethod] - public string recall_url { owned get; set; } - [NoAccessorMethod] - public string recall_vendor { owned get; set; } - [NoAccessorMethod] + [Version (since = "0.9.0")] public string serial { owned get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public uint state { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public uint technology { get; set; } [NoAccessorMethod] + [Version (since = "0.9.22")] public double temperature { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public int64 time_to_empty { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public int64 time_to_full { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public uint64 update_time { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public string vendor { owned get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double voltage { get; set; } - public virtual signal void changed (); + [NoAccessorMethod] + [Version (since = "1.0")] + public uint warning_level { get; set; } } [CCode (cheader_filename = "upower.h", type_id = "up_history_item_get_type ()")] public class HistoryItem : GLib.Object { [CCode (cname = "up_history_item_new", has_construct_function = false)] + [Version (since = "0.9.0")] public HistoryItem (); [CCode (cname = "up_history_item_get_state")] + [Version (since = "0.9.0")] public Up.DeviceState get_state (); [CCode (cname = "up_history_item_get_time")] + [Version (since = "0.9.0")] public uint get_time (); [CCode (cname = "up_history_item_get_value")] + [Version (since = "0.9.0")] public double get_value (); [CCode (cname = "up_history_item_set_from_string")] + [Version (since = "0.9.1")] public bool set_from_string (string text); [CCode (cname = "up_history_item_set_state")] + [Version (since = "0.9.0")] public void set_state (Up.DeviceState state); [CCode (cname = "up_history_item_set_time")] + [Version (since = "0.9.0")] public void set_time (uint time); [CCode (cname = "up_history_item_set_time_to_present")] + [Version (since = "0.9.1")] public void set_time_to_present (); [CCode (cname = "up_history_item_set_value")] + [Version (since = "0.9.0")] public void set_value (double value); [CCode (cname = "up_history_item_to_string")] + [Version (since = "0.9.1")] public string to_string (); [NoAccessorMethod] + [Version (since = "0.9.0")] public uint state { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public uint time { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double value { get; set; } } - [CCode (cheader_filename = "upower.h", type_id = "up_qos_item_get_type ()")] - public class QosItem : GLib.Object { - [CCode (cname = "up_qos_item_new", has_construct_function = false)] - public QosItem (); - [CCode (cname = "up_qos_item_get_cmdline")] - public unowned string get_cmdline (); - [CCode (cname = "up_qos_item_get_cookie")] - public uint get_cookie (); - [CCode (cname = "up_qos_item_get_kind")] - public Up.QosKind get_kind (); - [CCode (cname = "up_qos_item_get_persistent")] - public bool get_persistent (); - [CCode (cname = "up_qos_item_get_pid")] - public uint get_pid (); - [CCode (cname = "up_qos_item_get_sender")] - public unowned string get_sender (); - [CCode (cname = "up_qos_item_get_timespec")] - public uint64 get_timespec (); - [CCode (cname = "up_qos_item_get_uid")] - public uint get_uid (); - [CCode (cname = "up_qos_item_get_value")] - public int get_value (); - [CCode (cname = "up_qos_item_set_cmdline")] - public void set_cmdline (string cmdline); - [CCode (cname = "up_qos_item_set_cookie")] - public void set_cookie (uint cookie); - [CCode (cname = "up_qos_item_set_kind")] - public void set_kind (Up.QosKind type); - [CCode (cname = "up_qos_item_set_persistent")] - public void set_persistent (bool persistent); - [CCode (cname = "up_qos_item_set_pid")] - public void set_pid (uint pid); - [CCode (cname = "up_qos_item_set_sender")] - public void set_sender (string sender); - [CCode (cname = "up_qos_item_set_timespec")] - public void set_timespec (uint64 timespec); - [CCode (cname = "up_qos_item_set_uid")] - public void set_uid (uint uid); - [CCode (cname = "up_qos_item_set_value")] - public void set_value (int value); - [NoAccessorMethod] - public string cmdline { owned get; set; } - [NoAccessorMethod] - public uint cookie { get; set; } - [NoAccessorMethod] - public bool persistent { get; set; } - [NoAccessorMethod] - public uint pid { get; set; } - [NoAccessorMethod] - public string sender { owned get; set; } - [NoAccessorMethod] - public uint64 timespec { get; set; } - [NoAccessorMethod] - public uint type { get; set; } - [NoAccessorMethod] - public uint uid { get; set; } - [NoAccessorMethod] - public int value { get; set; } - } [CCode (cheader_filename = "upower.h", type_id = "up_stats_item_get_type ()")] public class StatsItem : GLib.Object { [CCode (cname = "up_stats_item_new", has_construct_function = false)] + [Version (since = "0.9.0")] public StatsItem (); [CCode (cname = "up_stats_item_get_accuracy")] + [Version (since = "0.9.0")] public double get_accuracy (); [CCode (cname = "up_stats_item_get_value")] + [Version (since = "0.9.0")] public double get_value (); [CCode (cname = "up_stats_item_set_accuracy")] + [Version (since = "0.9.0")] public void set_accuracy (double accuracy); [CCode (cname = "up_stats_item_set_value")] + [Version (since = "0.9.0")] public void set_value (double value); [NoAccessorMethod] + [Version (since = "0.9.0")] public double accuracy { get; set; } [NoAccessorMethod] + [Version (since = "0.9.0")] public double value { get; set; } } - [CCode (cheader_filename = "upower.h", type_id = "up_wakeup_item_get_type ()")] - public class WakeupItem : GLib.Object { - [CCode (cname = "up_wakeup_item_new", has_construct_function = false)] - public WakeupItem (); - [CCode (cname = "up_wakeup_item_get_cmdline")] - public unowned string get_cmdline (); - [CCode (cname = "up_wakeup_item_get_details")] - public unowned string get_details (); - [CCode (cname = "up_wakeup_item_get_id")] - public uint get_id (); - [CCode (cname = "up_wakeup_item_get_is_userspace")] - public bool get_is_userspace (); - [CCode (cname = "up_wakeup_item_get_old")] - public uint get_old (); - [CCode (cname = "up_wakeup_item_get_value")] - public double get_value (); - [CCode (cname = "up_wakeup_item_set_cmdline")] - public void set_cmdline (string cmdline); - [CCode (cname = "up_wakeup_item_set_details")] - public void set_details (string details); - [CCode (cname = "up_wakeup_item_set_id")] - public void set_id (uint id); - [CCode (cname = "up_wakeup_item_set_is_userspace")] - public void set_is_userspace (bool is_userspace); - [CCode (cname = "up_wakeup_item_set_old")] - public void set_old (uint old); - [CCode (cname = "up_wakeup_item_set_value")] - public void set_value (double value); - [NoAccessorMethod] - public string cmdline { owned get; set; } - [NoAccessorMethod] - public string details { owned get; set; } - [NoAccessorMethod] - public uint id { get; set; } - [NoAccessorMethod] - public bool is_userspace { get; set; } - [NoAccessorMethod] - public uint old { get; set; } - [NoAccessorMethod] - public double value { get; set; } - } - [CCode (cheader_filename = "upower.h", type_id = "up_wakeups_get_type ()")] - public class Wakeups : GLib.Object { - [CCode (cname = "up_wakeups_new", has_construct_function = false)] - public Wakeups (); - [CCode (cname = "up_wakeups_get_data_sync")] - public GLib.GenericArray get_data_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; - [CCode (cname = "up_wakeups_get_has_capability")] - public bool get_has_capability (); - [CCode (cname = "up_wakeups_get_properties_sync")] - public bool get_properties_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; - [CCode (cname = "up_wakeups_get_total_sync")] - public uint get_total_sync (GLib.Cancellable? cancellable = null) throws GLib.Error; - public virtual signal void data_changed (); - public virtual signal void total_changed (uint value); - } [CCode (cheader_filename = "upower.h", cprefix = "UP_DEVICE_KIND_", has_type_id = false)] public enum DeviceKind { UNKNOWN, @@ -323,6 +275,36 @@ namespace Up { MEDIA_PLAYER, TABLET, COMPUTER, + GAMING_INPUT, + PEN, + TOUCHPAD, + MODEM, + NETWORK, + HEADSET, + SPEAKERS, + HEADPHONES, + VIDEO, + OTHER_AUDIO, + REMOTE_CONTROL, + PRINTER, + SCANNER, + CAMERA, + WEARABLE, + TOY, + BLUETOOTH_GENERIC, + LAST + } + [CCode (cheader_filename = "upower.h", cprefix = "UP_DEVICE_LEVEL_", has_type_id = false)] + public enum DeviceLevel { + UNKNOWN, + NONE, + DISCHARGING, + LOW, + CRITICAL, + ACTION, + NORMAL, + HIGH, + FULL, LAST } [CCode (cheader_filename = "upower.h", cprefix = "UP_DEVICE_STATE_", has_type_id = false)] @@ -347,33 +329,10 @@ namespace Up { NICKEL_METAL_HYDRIDE, LAST } - [CCode (cheader_filename = "upower.h", cprefix = "UP_QOS_KIND_", has_type_id = false)] - public enum QosKind { - UNKNOWN, - NETWORK, - CPU_DMA, - LAST - } - [CCode (cheader_filename = "upower.h", cprefix = "UP_SLEEP_KIND_", has_type_id = false)] - public enum SleepKind { - UNKNOWN, - SUSPEND, - HIBERNATE, - HYBRID, - LAST - } [CCode (cheader_filename = "upower.h", cname = "UP_MAJOR_VERSION")] public const int MAJOR_VERSION; [CCode (cheader_filename = "upower.h", cname = "UP_MICRO_VERSION")] public const int MICRO_VERSION; [CCode (cheader_filename = "upower.h", cname = "UP_MINOR_VERSION")] public const int MINOR_VERSION; - [CCode (cheader_filename = "upower.h", cname = "up_qos_kind_from_string")] - public static Up.QosKind qos_kind_from_string (string type); - [CCode (cheader_filename = "upower.h", cname = "up_qos_kind_to_string")] - public static unowned string qos_kind_to_string (Up.QosKind type); - [CCode (cheader_filename = "upower.h", cname = "up_sleep_kind_from_string")] - public static Up.SleepKind sleep_kind_from_string (string sleep_kind); - [CCode (cheader_filename = "upower.h", cname = "up_sleep_kind_to_string")] - public static unowned string sleep_kind_to_string (Up.SleepKind sleep_kind_enum); } From 29f9614afd0a3ef7ca869d730318907e6ea86581 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 17 Jan 2023 17:12:38 -0500 Subject: [PATCH 02/81] Add the public-facing Bluetooth API Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 5286e9cdd..2b77937b0 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -1078,4 +1078,91 @@ class BluetoothClient : GLib.Object { return true; } + + /** + * Returns a ListStore representing the devices connected to the current adapter. + */ + public ListStore get_devices() { + return list_store; + } + + /** + * Starts pairing a Bluetooth device. + */ + public async void setup_device(string path) throws DBusError, IOError { + var device = get_device_for_path(path); + + if (device == null) return; + + var proxy = device.proxy; + var device1 = proxy as Device1; + + yield device1.Pair(); + } + + /** + * Cancels pairing a Bluetooth device. + */ + public async void cancel_setup_device(string path) throws DBusError, IOError { + var device = get_device_for_path(path); + + if (device == null) return; + + var proxy = device.proxy; + var device1 = proxy as Device1; + + yield device1.CancelPairing(); + } + + /** + * Sets whether or not a Bluetooth device is trusted. + */ + public void set_trusted(string path, bool trusted) { + var device = get_device_for_path(path); + + if (device == null) return; + + device.trusted = trusted; + } + + /** + * Starts connecting to one of the known-connectable services on a device. + */ + public async void connect_service(string path, bool connect) throws DBusError, IOError { + var device = get_device_for_path(path); + + if (device == null) return; + + var proxy = device.proxy; + var device1 = proxy as Device1; + + if (connect) { + yield device1.Connect(); + } else { + yield device1.Disconnect(); + } + } + + /** + * Returns whether or not there are Bluetooth devices connected that have input capabilities. + */ + public bool has_connected_input_devices() { + var connected = false; + var num_items = list_store.get_n_items(); + + for (var i = 0; i < num_items; i++) { + var obj = list_store.get_item(i); + var device = obj as BluetoothDevice; + + if (!device.connected) continue; + if (device.uuids == null || device.uuids.length == 0) continue; + + if ("Human Interface Device" in device.uuids || "HumanInterfaceDeviceService" in device.uuids) { + connected = true; + break; + } + } + + return connected; + } } From 891dea957831e7748c0c6e7042edba0f2f2a6fed Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 17 Jan 2023 17:16:06 -0500 Subject: [PATCH 03/81] Fix last TODO item Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 2b77937b0..eec728c5f 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -734,7 +734,11 @@ class BluetoothClient : GLib.Object { // Handle a removal if (change_type == REMOVED) { - // TODO: Clear the removed_devices queue + if (removed_devices_id != 0) { + Source.remove(removed_devices_id); + removed_devices_id = 0; + } + removed_devices.clear(); } default_adapter_changed(new_default_adapter, change_type); From 820d71634ccb4d6cec4acc7683fd99815b75a954 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 17 Jan 2023 17:34:45 -0500 Subject: [PATCH 04/81] Fix warnings Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 23 +++++++--------- src/panel/applets/status/BluetoothDBus.vala | 26 +++++++++---------- src/panel/applets/status/BluetoothDevice.vala | 6 ++--- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index eec728c5f..9c2896af6 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -105,7 +105,7 @@ class BluetoothClient : GLib.Object { } } - BluetoothClient() { + public BluetoothClient() { Object(); } @@ -302,12 +302,7 @@ class BluetoothClient : GLib.Object { /** * Get the type of a Bluetooth device, and use that type to get an icon for it. */ - private void get_type_and_icon_for_device(Device1 device, out BluetoothType? type, out string? icon) { - if (type != 0 || icon != null) { - warning("Attempted to get type and icon for device '%s', but type or icon is not 0 or null", device.Name); - return; - } - + private void get_type_and_icon_for_device(Device1 device, out BluetoothType type, out string icon) { // Special case these joypads if (device.Name == "ION iCade Game Controller" || device.Name == "8Bitdo Zero GamePad") { type = BluetoothType.JOYPAD; @@ -316,9 +311,7 @@ class BluetoothClient : GLib.Object { } // First, try to match the appearance of the device - if (type == BluetoothType.ANY) { - type = appearance_to_type(device.Appearance); - } + type = appearance_to_type(device.Appearance); // Match on the class if the appearance failed if (type == BluetoothType.ANY) { type = class_to_type(device.Class); @@ -381,10 +374,12 @@ class BluetoothClient : GLib.Object { get_type_and_icon_for_device(device1, out type, out icon); - device.type = type; + device.device_type = type; device.icon = icon; + break; default: debug("Not handling property '%s'", property); + break; } } @@ -667,15 +662,19 @@ class BluetoothClient : GLib.Object { switch (property) { case "alias": default_adapter_name = adapter.Alias; + break; case "discovering": discovery_started = adapter.Discovering; + break; case "powered": default_adapter_powered = adapter.Powered; if (!has_power_state) { default_adapter_state = get_state(); } + break; case "power-state": default_adapter_state = get_state(); + break; } } @@ -975,7 +974,6 @@ class BluetoothClient : GLib.Object { default: return OTHER_AUDIO; } - break; } return ANY; @@ -1017,7 +1015,6 @@ class BluetoothClient : GLib.Object { default: return OTHER_AUDIO; } - break; case 0x05: switch ((klass & 0xc0) >> 6) { case 0x00: diff --git a/src/panel/applets/status/BluetoothDBus.vala b/src/panel/applets/status/BluetoothDBus.vala index 5892cb39c..28b0228ca 100644 --- a/src/panel/applets/status/BluetoothDBus.vala +++ b/src/panel/applets/status/BluetoothDBus.vala @@ -15,19 +15,19 @@ */ [DBus (name="org.bluez.Adapter1")] public interface Adapter1 : GLib.Object { - public abstract string Address { get; } - public abstract string Name { get; } - public abstract string Alias { get; set; } + public abstract string Address { owned get; } + public abstract string Name { owned get; } + public abstract string Alias { owned get; set; } public abstract uint32 Class { get; } public abstract bool Powered { get; set; } - public abstract string PoweredState { get; } + public abstract string PoweredState { owned get; } public abstract bool Discoverable { set; get; } public abstract uint32 DiscoverableTimeout { get; set; } public abstract bool Pairable { get; set; } public abstract uint32 PairableTimeout { get; set; } public abstract bool Discovering { get; set; } - public abstract string[] UUIDS { get; } - public abstract string Modalias { get; } + public abstract string[] UUIDS { owned get; } + public abstract string Modalias { owned get; } public async abstract void StartDiscovery() throws GLib.DBusError, GLib.IOError; public async abstract void StopDiscovery() throws GLib.DBusError, GLib.IOError; @@ -40,21 +40,21 @@ public interface Adapter1 : GLib.Object { */ [DBus (name = "org.bluez.Device1")] public interface Device1 : GLib.Object { - public abstract string Address { get; } - public abstract string Name { get; } - public abstract string Alias { get; set; } + public abstract string Address { owned get; } + public abstract string Name { owned get; } + public abstract string Alias { owned get; set; } public abstract uint32 Class { get; } public abstract uint16 Appearance { get; } - public abstract string Icon { get; } + public abstract string Icon { owned get; } public abstract bool Paired { get; } public abstract bool Trusted { get; set; } public abstract bool Blocked { get; set; } public abstract bool LegacyPairing { get; } public abstract int16 RSSI { get; } public abstract bool Connected { get; } - public abstract string[] UUIDs { get; } - public abstract string Modalias { get; } - public abstract GLib.ObjectPath Adapter { get; } + public abstract string[] UUIDs { owned get; } + public abstract string Modalias { owned get; } + public abstract GLib.ObjectPath Adapter { owned get; } public async abstract void Connect() throws GLib.DBusError, GLib.IOError; public async abstract void Disconnect() throws GLib.DBusError, GLib.IOError; diff --git a/src/panel/applets/status/BluetoothDevice.vala b/src/panel/applets/status/BluetoothDevice.vala index cc366f05a..332bc16e4 100644 --- a/src/panel/applets/status/BluetoothDevice.vala +++ b/src/panel/applets/status/BluetoothDevice.vala @@ -20,7 +20,7 @@ public class BluetoothDevice : Object { public string address { get; set; } public string alias { get; set; } public string name { get; set; } - public BluetoothType type { get; set; default = BluetoothType.ANY; } + public BluetoothType device_type { get; set; default = BluetoothType.ANY; } public string icon { get; set; } public bool paired { get; set; default = false; } public bool trusted { get; set; default = false; } @@ -42,7 +42,7 @@ public class BluetoothDevice : Object { address: device.Address, alias: device.Alias, name: device.Name, - type: type, + device_type: type, icon: icon, legacy_pairing: device.LegacyPairing, uuids: device.UUIDs, @@ -90,7 +90,7 @@ public class BluetoothDevice : Object { } battery_type = type; - battery_level = up_device.battery_level as DeviceLevel; + battery_level = (DeviceLevel) up_device.battery_level; battery_percentage = up_device.percentage; } } From 5f7d4d1c4c3309f8b57fec75f9d5b86f57b82c36 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 17 Jan 2023 18:34:06 -0500 Subject: [PATCH 05/81] WIP: Redo Bluetooth indicator and popover Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 135 ++---------------- 1 file changed, 12 insertions(+), 123 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 00ec9a97e..b328f168d 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -14,29 +14,21 @@ public class BluetoothIndicator : Gtk.Bin { public Gtk.Image? image = null; - public Gtk.EventBox? ebox = null; -#if with_bluetooth - private Bluetooth.Client? client = null; -#endif - private Gtk.TreeModel? model = null; public Budgie.Popover? popover = null; + private Gtk.CheckButton radio_airplane; + private ulong radio_id; + private Gtk.Button send_to; - Gtk.CheckButton radio_airplane; - ulong radio_id; - Gtk.Button send_to; + private BluetoothClient client; public BluetoothIndicator() { image = new Gtk.Image.from_icon_name("bluetooth-active-symbolic", Gtk.IconSize.MENU); ebox = new Gtk.EventBox(); - add(ebox); - ebox.add(image); - ebox.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK); - ebox.button_release_event.connect(on_button_release_event); // Create our popover popover = new Budgie.Popover(ebox); @@ -69,123 +61,20 @@ public class BluetoothIndicator : Gtk.Bin { // Ensure all content is shown box.show_all(); -#if with_bluetooth - client = new Bluetooth.Client(); - model = client.get_model(); -#endif - model.row_changed.connect(() => { resync(); }); - model.row_deleted.connect(() => { resync(); }); - model.row_inserted.connect(() => { resync(); }); - - this.resync(); - - this.setup_dbus.begin(() => { + // Create our Bluetooth client + client = new BluetoothClient(); + client.device_added.connect((device) => { + message("Bluetooth device added: %s", device.alias); + }); + client.device_removed.connect((path) => { + message("Bluetooth device removed: %s", path); }); + add(ebox); show_all(); } - private bool on_button_release_event(Gdk.EventButton e) { - if (e.button == Gdk.BUTTON_MIDDLE) { // Middle click - - } else { - return Gdk.EVENT_PROPAGATE; - } - - return Gdk.EVENT_STOP; - } - - async void setup_dbus() { - try { - } catch (Error e) { - return; - } - } - - bool get_default_adapter(out Gtk.TreeIter? adapter) { - adapter = null; - Gtk.TreeIter iter; - - if (!model.get_iter_first(out iter)) { - return false; - } - -#if with_bluetooth - while (true) { - bool is_default; - model.get(iter, Bluetooth.Column.DEFAULT, out is_default, -1); - if (is_default) { - adapter = iter; - return true; - } - if (!model.iter_next(ref iter)) { - break; - } - } -#endif - return false; - } - - int get_n_devices() { - Gtk.TreeIter iter; - Gtk.TreeIter? adapter; - int n_devices = 0; - - if (!get_default_adapter(out adapter)) { - return -1; - } - - if (!model.iter_children(out iter, adapter)) { - return 0; - } - -#if with_bluetooth - while (true) { - bool con = true; - model.get(iter, Bluetooth.Column.CONNECTED, out con, -1); - if (con) { - n_devices++; - } - if (!model.iter_next(ref iter)) { - break; - } - } - -#endif - return n_devices; - } - - private void resync() { - var n_devices = get_n_devices(); - string? lbl = null; - - if (killer != null) { - if (killer.BluetoothAirplaneMode) { - image.set_from_icon_name("bluetooth-disabled-symbolic", Gtk.IconSize.MENU); - lbl = _("Bluetooth is disabled"); - n_devices = 0; - } else { - image.set_from_icon_name("bluetooth-active-symbolic", Gtk.IconSize.MENU); - lbl = _("Bluetooth is active"); - } - } - - if (n_devices > 0) { - lbl = ngettext("Connected to %d device", "Connected to %d devices", n_devices).printf(n_devices); - send_to.set_sensitive(true); - } else if (n_devices < 0) { - hide(); - return; - } else { - send_to.set_sensitive(false); - } - - /* TODO: Determine if bluetooth is actually active (rfkill) */ - show(); - image.set_tooltip_text(lbl); - } - void on_settings_activate() { this.popover.hide(); From 672501afedd41fe27a468b9c5b2fbe7a57ee16e8 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 18 Jan 2023 11:27:41 -0500 Subject: [PATCH 06/81] Fix Bluetooth setup Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 25 ++++++++++--------- .../applets/status/BluetoothIndicator.vala | 4 +-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 9c2896af6..d19ecfc04 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -80,7 +80,7 @@ class BluetoothClient : GLib.Object { private Queue removed_devices; private uint removed_devices_id = 0; - public signal void device_added(BluetoothDevice device); + public signal void device_added(string path); public signal void device_removed(string path); construct { @@ -115,18 +115,18 @@ class BluetoothClient : GLib.Object { } } + [CCode (cname = "adapter1_proxy_get_type")] + extern static Type get_adapter_proxy_type(); + + [CCode (cname = "device1_proxy_get_type")] + extern static Type get_device_proxy_type(); + private Type get_proxy_type_func(DBusObjectManagerClient manager, string object_path, string? interface_name) { - if (interface_name == null) { - return typeof(DBusObjectProxy); - } + if (interface_name == null) return typeof(DBusObjectProxy); - if (interface_name == BLUEZ_ADAPTER_INTERFACE) { - return typeof(Adapter1); - } + if (interface_name == BLUEZ_ADAPTER_INTERFACE) return get_adapter_proxy_type(); - if (interface_name == BLUEZ_DEVICE_INTERFACE) { - return typeof(Device1); - } + if (interface_name == BLUEZ_DEVICE_INTERFACE) return get_device_proxy_type(); return typeof(DBusProxy); } @@ -405,6 +405,7 @@ class BluetoothClient : GLib.Object { } Device1 device = iface as Device1; + var device_proxy = device as DBusProxy; if (device.Adapter != default_adapter_path) { continue; @@ -427,7 +428,7 @@ class BluetoothClient : GLib.Object { list_store.append(device_obj); // Emit device-added signal - device_added(device_obj); + device_added(device_proxy.get_object_path()); } if (coldplug_upower) { @@ -614,7 +615,7 @@ class BluetoothClient : GLib.Object { device_object = new BluetoothDevice(device, type, icon); list_store.append(device_object); - device_added(device_object); + device_added(device_proxy.get_object_path()); } /** diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index b328f168d..7365ac7d2 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -63,8 +63,8 @@ public class BluetoothIndicator : Gtk.Bin { // Create our Bluetooth client client = new BluetoothClient(); - client.device_added.connect((device) => { - message("Bluetooth device added: %s", device.alias); + client.device_added.connect((path) => { + message("Bluetooth device added: %s", path); }); client.device_removed.connect((path) => { From c4f9d824b1254bb144d69db52a34fd8130537e3b Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 18 Jan 2023 17:43:30 -0500 Subject: [PATCH 07/81] Greatly simplify the BluetoothClient API At this time, I have no idea how to integrate the UPower aspect with the Bluetooth devices. Moreover, I can't test if it's working even if I did, because none of my Bluetooth devices trigger the UPower signals. Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 979 ++++-------------- .../applets/status/BluetoothIndicator.vala | 8 +- 2 files changed, 228 insertions(+), 759 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index d19ecfc04..b178d7e09 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -9,7 +9,7 @@ * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * - * Inspired by gnome-bluetooth. + * Inspired by gnome-bluetooth and Elementary. */ using GLib; @@ -20,93 +20,38 @@ const string BLUEZ_MANAGER_PATH = "/"; const string BLUEZ_ADAPTER_INTERFACE = "org.bluez.Adapter1"; const string BLUEZ_DEVICE_INTERFACE = "org.bluez.Device1"; -const uint DEVICE_REMOVAL_TIMEOUT = 50; - -enum AdapterChangeType { - OWNER_UPDATE, - REPLACEMENT, - NEW_DEFAULT, - REMOVED -} - -[DBus (name="org.freedesktop.DBus.ObjectManager")] -public interface BluezManager : GLib.Object { - public abstract HashTable>> GetManagedObjects() throws GLib.DBusError, GLib.IOError; -} - class BluetoothClient : GLib.Object { - Cancellable cancellable; - ListStore list_store; - - DBusObjectManagerClient dbus_object_manager = null; - - public uint num_adapters { get; private set; default = 0; } - public Adapter1 default_adapter { get; private set; default = null; } - public PowerState default_adapter_state { get; private set; default = ABSENT; } - public bool discovery_started { get; set; default = false; } - public string default_adapter_name { get; private set; default = null; } - public string default_adapter_address { get; private set; default = null; } - - private bool _default_adapter_powered = false; - public bool default_adapter_powered { - get { return _default_adapter_powered; } - set { - if (default_adapter == null) return; - if (default_adapter.Powered == value) return; - - var proxy = default_adapter as DBusProxy; - var variant = new Variant.boolean(value); - proxy.call.begin( - "org.freedesktop.DBus.Properties.Set", - new Variant("(ssv)", "org.bluez.Adapter1", "Powered", variant), - DBusCallFlags.NONE, - -1, - null, - adapter_set_powered_cb - ); - } - } - - private bool _default_adapter_setup_mode = false; - public bool default_adapter_setup_mode { - get { return _default_adapter_setup_mode; } - set { set_adapter_discovering(value); } - } + private Cancellable cancellable; + private DBusObjectManagerClient object_manager; private Client upower_client; + + private HashTable upower_devices; + private bool bluez_devices_coldplugged = false; - private bool has_power_state = true; - private Queue removed_devices; - private uint removed_devices_id = 0; + public bool has_adapter { get; private set; default = false; } + public bool is_connected { get; private set; default = false; } + public bool is_enabled { get; private set; default = false; } + public bool is_powered { get; private set; default = false; } + public bool retrieve_finished { get; private set; default = false; } - public signal void device_added(string path); - public signal void device_removed(string path); + /** Signal emitted when a Bluetooth device has been added. */ + public signal void device_added(Device1 device); + /** Signal emitted when a Bluetooth device has been removed. */ + public signal void device_removed(Device1 device); + /** Signal emitted when our powered or connected state changes. */ + public signal void global_state_changed(bool enabled, bool connected); construct { - this.cancellable = new Cancellable(); - this.list_store = new ListStore(typeof(Device1)); - removed_devices = new Queue(); + cancellable = new Cancellable(); + upower_devices = new HashTable(str_hash, str_equal); // Set up our UPower client - try { - make_upower_client.begin(cancellable, make_upower_cb); - } catch (Error e) { - critical("error creating UPower client: %s", e.message); - return; - } - - // Begin creating our DBus Object Manager for Bluez - try { - this.make_dbus_object_manager.begin(make_client_cb); - } catch (Error e) { - critical("error getting DBusObjectManager for Bluez: %s", e.message); - return; - } - } + create_upower_client.begin(); - public BluetoothClient() { - Object(); + // Creating our DBus Object Manager for Bluez + create_object_manager.begin(); } ~BluetoothClient() { @@ -131,136 +76,83 @@ class BluetoothClient : GLib.Object { return typeof(DBusProxy); } - private async Client make_upower_client(Cancellable cancellable) throws Error { - return yield new Client.async(cancellable); - } - - private async DBusObjectManagerClient make_dbus_object_manager() throws Error { - return yield new DBusObjectManagerClient.for_bus( - BusType.SYSTEM, - DBusObjectManagerClientFlags.DO_NOT_AUTO_START, - BLUEZ_DBUS_NAME, - BLUEZ_MANAGER_PATH, - this.get_proxy_type_func, - this.cancellable - ); - } - - private void start_discovery_cb(Object? obj, AsyncResult? res) { + /** + * Create and setup our UPower client. + */ + private async void create_upower_client() { try { - default_adapter.StartDiscovery.end(res); - } catch (Error e) { - var proxy = default_adapter as DBusProxy; - warning("Error calling StartDiscovery() on '%s' org.bluez.Adapter1: %s (%s %d)", proxy.get_object_path(), e.message, e.domain.to_string(), e.code); - discovery_started = false; - } - } + upower_client = yield new Client.async(cancellable); - private void stop_discovery_cb(Object? obj, AsyncResult? res) { - try { - default_adapter.StopDiscovery.end(res); - } catch (Error e) { - var proxy = default_adapter as DBusProxy; - warning("Error calling StopDiscovery() on '%s': %s (%s %d)", proxy.get_object_path(), e.message, e.domain.to_string(), e.code); - discovery_started = false; - } - } + // Connect the signals + upower_client.device_added.connect(upower_device_added_cb); + upower_client.device_removed.connect(upower_device_removed_cb); - private void set_discovery_filter_cb(Object? object, AsyncResult? res) { - try { - default_adapter.SetDiscoveryFilter.end(res); - } catch (Error e) { - warning("Error calling SetDiscoveryFilter() on interface org.bluez.Adapter1: %s (%s %d)", e.message, e.domain.to_string(), e.code); - discovery_started = false; - return; + // Maybe coldplug UPower devices + if (bluez_devices_coldplugged) { + coldplug_client(); } - - var proxy = default_adapter as DBusProxy; - debug("Starting discovery on %s", proxy.get_object_path()); - default_adapter.StartDiscovery.begin(start_discovery_cb); - } - - private void set_adapter_discovering(bool discovering) { - if (discovery_started) return; - if (default_adapter == null) return; - - var proxy = default_adapter as DBusProxy; - - discovery_started = discovering; - - if (discovering) { - var properties = new HashTable(str_hash, str_equal); - properties["Discoverable"] = discovering; - default_adapter.SetDiscoveryFilter.begin(properties, set_discovery_filter_cb); - } else { - debug("Stopping discovery on %s", proxy.get_object_path()); - default_adapter.StopDiscovery.begin(stop_discovery_cb); + } catch (Error e) { + critical("Error creating UPower client: %s", e.message); } } /** - * Get the device in our list model with the given path. - * - * If no device is found with the same path, `null` is returned. + * Create and setup our Bluez DBus object manager client. */ - private BluetoothDevice? get_device_for_path(string path) { - BluetoothDevice? device = null; - - var num_items = list_store.get_n_items(); - for (int i = 0; i < num_items; i++) { - var d = list_store.get_item(i) as BluetoothDevice; - if (path == d.get_object_path()) { - device = d; - break; - } + private async void create_object_manager() { + try { + object_manager = yield new DBusObjectManagerClient.for_bus( + BusType.SYSTEM, + DBusObjectManagerClientFlags.NONE, + BLUEZ_DBUS_NAME, + BLUEZ_MANAGER_PATH, + this.get_proxy_type_func, + this.cancellable + ); + + // Add all of the current interfaces + object_manager.get_objects().foreach((object) => { + object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); + }); + + // Connect the signals + object_manager.interface_added.connect(on_interface_added); + object_manager.interface_removed.connect(on_interface_removed); + + object_manager.object_added.connect((object) => { + object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); + }); + object_manager.object_removed.connect((object) => { + object.get_interfaces().foreach((iface) => on_interface_removed(object, iface)); + }); + } catch (Error e) { + critical("Error getting DBus Object Manager: %s", e.message); } - return device; + retrieve_finished = true; } - /** - * Get the device in our list model with the given address. - * - * If no device is found with the same address, `null` is returned. - */ - private BluetoothDevice? get_device_for_address(string address) { - BluetoothDevice? device = null; - - var num_items = list_store.get_n_items(); - for (int i = 0; i < num_items; i++) { - var d = list_store.get_item(i) as BluetoothDevice; - if (address == d.address) { - device = d; - break; - } - } + // private BluetoothDevice? get_device_with_address(string address) { + // var num_items = devices.get_n_items(); - return device; - } + // for (var i = 0; i < num_items; i++) { + // var device = devices.get_item(i) as BluetoothDevice; + // if (device.address == address) return device; + // } - /** - * Get the device in our list model with the given UPower device path. - * - * If no device is found with the same path, `null` is returned. - */ - private BluetoothDevice? get_device_for_upower_device(string path) { - BluetoothDevice? device = null; + // return null; + // } - var num_items = list_store.get_n_items(); - for (int i = 0; i < num_items; i++) { - var d = list_store.get_item(i) as BluetoothDevice; - var up_device = d.get_upower_device(); + // private BluetoothDevice? get_device_with_object_path(string object_path) { + // var num_items = devices.get_n_items(); - if (up_device == null) { - continue; - } - if (up_device.get_object_path() == path) { - device = d; - } - } + // for (var i = 0; i < num_items; i++) { + // var device = devices.get_item(i) as BluetoothDevice; + // if (device.get_object_path() == object_path) return device; + // } - return device; - } + // return null; + // } /** * Tries to get an icon name present in GTK themes for a Bluetooth type. @@ -332,495 +224,52 @@ class BluetoothClient : GLib.Object { } /** - * Handle property changes for a Bluetooth device. - */ - private void device_notify_cb(Object obj, ParamSpec pspec) { - Device1 device1 = obj as Device1; - DBusProxy proxy = device1 as DBusProxy; - var property = pspec.name; - - var path = proxy.get_object_path(); - var device = get_device_for_path(path); - - if (device == null) { - debug("Device '%s' not found, ignoring property change for '%s'", path, property); - return; - } - - switch (property) { - case "name": - device.name = device1.Name; - break; - case "alias": - device.alias = device1.Alias; - break; - case "paired": - device.trusted = device1.Trusted; - break; - case "connected": - device.connected = device1.Connected; - break; - case "uuids": - device.uuids = device1.UUIDs; - break; - case "legacy-pairing": - device.legacy_pairing = device1.LegacyPairing; - break; - case "icon": - case "class": - case "appearance": - BluetoothType type = BluetoothType.ANY; - string? icon = null; - - get_type_and_icon_for_device(device1, out type, out icon); - - device.device_type = type; - device.icon = icon; - break; - default: - debug("Not handling property '%s'", property); - break; - } - } - - private void add_devices_to_list_store() { - var coldplug_upower = !bluez_devices_coldplugged && upower_client != null; - - debug("Emptying device list store since default adapter changed"); - list_store.remove_all(); - - DBusProxy proxy = default_adapter as DBusProxy; - var default_adapter_path = proxy.get_object_path(); - - debug("Coldplugging devices for new default adapter"); - - bluez_devices_coldplugged = true; - var object_list = dbus_object_manager.get_objects(); - - // Add each device from DBus - foreach (var obj in object_list) { - var iface = obj.get_interface(BLUEZ_DEVICE_INTERFACE); - if (iface == null) { - continue; - } - - Device1 device = iface as Device1; - var device_proxy = device as DBusProxy; - - if (device.Adapter != default_adapter_path) { - continue; - } - - // Connect device 'notify' signal for property changes - device.notify.connect(device_notify_cb); - - // Resolve device type and icon - BluetoothType type = BluetoothType.ANY; - string? icon = null; - get_type_and_icon_for_device(device, out type, out icon); - - debug("Adding device '%s' on adapter '%s' to list store", device.Address, device.Adapter); - - // Create Device object - var device_obj = new BluetoothDevice(device, type, icon); - - // Append to list_store - list_store.append(device_obj); - - // Emit device-added signal - device_added(device_proxy.get_object_path()); - } - - if (coldplug_upower) { - coldplug_client(); - } - } - - /** - * Get the power state of the current default adapter. - */ - private PowerState get_state() { - if (default_adapter == null) { - return PowerState.ABSENT; - } - - var state = default_adapter.PoweredState; - - // Check if we have a valid power state - if (state == null) { - has_power_state = false; - - // Fallback to either on or off - return default_adapter.Powered ? PowerState.ON : PowerState.OFF; - } - - return PowerState.from_string(state); - } - - private bool is_default_adapter(Adapter1? adapter) { - if (this.default_adapter == null) { - return false; - } - - if (adapter == null) { - return false; - } - - DBusProxy adapter_proxy = adapter as DBusProxy; - DBusProxy default_proxy = default_adapter as DBusProxy; - - return (adapter_proxy.get_object_path() == default_proxy.get_object_path()); - } - - private bool should_be_default_adapter(Adapter1 adapter) { - DBusProxy proxy = adapter as DBusProxy; - DBusProxy default_proxy = this.default_adapter as DBusProxy; - - return proxy.get_object_path() == default_proxy.get_object_path(); - } - - /** - * Reset the default_adapter properties to their defaults. - */ - private void reset_default_adapter_props() { - default_adapter = null; - default_adapter_address = null; - default_adapter_powered = false; - default_adapter_state = PowerState.ABSENT; - discovery_started = false; - default_adapter_name = null; - } - - /** - * Updates the default_adapter_* properties from the current default adapter. - */ - private void update_default_adapter_props() { - default_adapter_address = default_adapter.Address; - default_adapter_powered = default_adapter.Powered; - default_adapter_state = PowerState.from_string(default_adapter.PoweredState); - discovery_started = default_adapter.Discovering; - default_adapter_name = default_adapter.Name; - } - - /** - * Handles when the default Bluetooth adapter changes. - */ - private void default_adapter_changed(DBusProxy proxy, AdapterChangeType change_type) { - Adapter1 adapter = proxy as Adapter1; - - switch (change_type) { - case REMOVED: - reset_default_adapter_props(); - list_store.remove_all(); - return; - case REPLACEMENT: - list_store.remove_all(); - set_adapter_discovering(false); - default_adapter = null; - break; - default: // Handles new default and owner update cases - default_adapter = null; - break; - } - - default_adapter = adapter; - adapter.notify.connect(adapter_notify_cb); - - // Bail if the change was only an update - if (change_type == OWNER_UPDATE) { - return; - } - - add_devices_to_list_store(); - update_default_adapter_props(); - } - - private void adapter_set_powered_cb(Object? obj, AsyncResult? res) { - var proxy = default_adapter as DBusProxy; - - try { - proxy.call.end(res); - } catch (Error e) { - warning("Error setting property 'Powered' on %s: %s (%s, %d)", proxy.get_object_path(), e.message, e.domain.to_string(), e.code); - } - } - - /** - * Process the device removal queue, removing all devices with paths - * in the queue from our list store. - */ - private bool unqueue_device_removal() { - if (removed_devices == null || removed_devices.is_empty()) return Source.REMOVE; - - // Iterate over the queue - string? path = null; - while ((path = removed_devices.pop_head()) != null) { - var found = false; - var num_items = list_store.get_n_items(); - - debug("Processing '%s' in removal queue", path); - - // Iterate over our list store to try to find the correct device - for (var i = 0; i < num_items; i++) { - var device = list_store.get_item(i) as BluetoothDevice; - - // Check if the path for this device matches the current queue item - if (path != device.get_object_path()) continue; - - // Matching device was found, remove it - device_removed(path); - list_store.remove(i); - found = true; - break; - } - - if (!found) debug("Device %s not known, ignoring", path); - } - - // Clear any remaining devices from the queue - removed_devices.clear(); - return Source.REMOVE; - } - - /** - * Adds a new Bluetooth device to our list store, or updates an - * existing one if it already exists. + * Handles the addition of a DBus object interface. */ - private void add_device(Device1 device) { - var adapter_path = device.Adapter; - var default_adapter_proxy = default_adapter as DBusProxy; - var default_adapter_path = default_adapter_proxy.get_object_path(); - - // Ensure that the device is on the current default adapter - if (adapter_path != default_adapter_path) return; + private void on_interface_added(DBusObject object, DBusInterface iface) { + if (iface is Adapter1) { + unowned Adapter1 adapter = iface as Adapter1; + + ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { + var powered = changed.lookup_value("Powered", new VariantType("b")); + if (powered == null) return; + set_last_powered.begin(); + }); + } else if (iface is Device1) { + unowned Device1 device = iface as Device1; + + if (device.Paired) device_added(device); + + ((DBusProxy) device).g_properties_changed.connect((changed, invalid) => { + var connected = changed.lookup_value("Connected", new VariantType("b")); + if (connected != null) { + check_powered(); + } - device.notify.connect(device_notify_cb); + var paired = changed.lookup_value("Paired", new VariantType("b")); + if (paired == null) return; - var device_proxy = device as DBusProxy; - var device_path = device_proxy.get_object_path(); - var device_object = get_device_for_path(device_path); + // Add or remove the device if it is paired or not + if (device.Paired) { + upower_devices[((DBusProxy) device).get_object_path()] = null; + device_added(device); + } else device_removed(device); - // Update the device if it's already been added - if (device_object != null) { - debug("Updating proxy for device '%s'", device_path); - device_object.proxy = device_proxy; - return; + check_powered(); + }); } - - BluetoothType type = 0; - string? icon = null; - get_type_and_icon_for_device(device, out type, out icon); - - debug("Adding device '%s' to adapter '%s'", device.Address, adapter_path); - - device_object = new BluetoothDevice(device, type, icon); - list_store.append(device_object); - device_added(device_proxy.get_object_path()); } /** - * Adds a device to the queue for removal. + * Handles the removal of a DBus object interface. */ - private void queue_remove_device(string path) { - debug("Queueing removal of device %s", path); - removed_devices.push_head(path); - - // Remove the current task to process the queue, if any - if (removed_devices_id != 0) { - Source.remove(removed_devices_id); - } - - // Add a task to process the queue - removed_devices_id = Timeout.add(DEVICE_REMOVAL_TIMEOUT, unqueue_device_removal); - } - - /** - * Handles property changes on a Bluetooth adapter. - * - * If the adapter is not the current default adapter, then - * nothing is updated. - */ - private void adapter_notify_cb(Object obj, ParamSpec pspec) { - Adapter1 adapter = obj as Adapter1; - DBusProxy proxy = adapter as DBusProxy; - - var property = pspec.name; - var adapter_path = proxy.get_object_path(); - - if (default_adapter == null) { - debug("Property '%s' changed on adapter '%s', but default adapter not set yet", property, adapter_path); - return; - } - - if (adapter != default_adapter) { - debug("Ignoring property change '%s' change on non-default adapter '%s'", property, adapter_path); - return; - } - - debug("Property change received for adapter '%s': %s", adapter_path, property); - - // Update the client property that changed on the adapter - switch (property) { - case "alias": - default_adapter_name = adapter.Alias; - break; - case "discovering": - discovery_started = adapter.Discovering; - break; - case "powered": - default_adapter_powered = adapter.Powered; - if (!has_power_state) { - default_adapter_state = get_state(); - } - break; - case "power-state": - default_adapter_state = get_state(); - break; - } - } - - private void add_adapter(Adapter1 adapter) { - DBusProxy proxy = adapter as DBusProxy; - - var name = proxy.get_name_owner(); - var iface = proxy.get_interface_name(); - var path = proxy.get_object_path(); - - if (this.default_adapter == null) { - debug("Adding adapter %s %s %s", name, path, iface); - default_adapter_changed(proxy, NEW_DEFAULT); - } else if (is_default_adapter(adapter)) { - debug("Updating default adapter with new proxy %s %s %s", name, path, iface); - default_adapter_changed(proxy, OWNER_UPDATE); - } else if (should_be_default_adapter(adapter)) { - var default_proxy = default_adapter as DBusProxy; - debug("Replacing adapter %s with %s %s %s", default_proxy.get_name_owner(), name, path, iface); - default_adapter_changed(proxy, REPLACEMENT); - } else { - debug("Ignoring non-default adapter %s %s %s", name, path, iface); - return; - } - - this.num_adapters++; - } - - private void adapter_removed(string path) { - DBusProxy default_proxy = this.default_adapter as DBusProxy; - DBusProxy new_default_adapter = null; - bool was_default = false; - - // Check if this is the path to the current default adapter - if (strcmp(path, default_proxy.get_object_path()) == 0) { - was_default = true; - } - - if (was_default) { - this.num_adapters--; - return; - } - - // Look through the list of DBus objects for a new default adapter - var object_list = this.dbus_object_manager.get_objects(); - foreach (var object in object_list) { - var iface = object.get_interface(BLUEZ_ADAPTER_INTERFACE); - if (iface != null) { - new_default_adapter = iface as DBusProxy; - break; - } - } - - // Decide if we have a removal, or if we have a new default - var change_type = new_default_adapter == null ? AdapterChangeType.REMOVED : AdapterChangeType.NEW_DEFAULT; - - // Handle a removal - if (change_type == REMOVED) { - if (removed_devices_id != 0) { - Source.remove(removed_devices_id); - removed_devices_id = 0; - } - removed_devices.clear(); - } - - default_adapter_changed(new_default_adapter, change_type); - this.num_adapters--; - } - - private void interface_added(DBusObject object, DBusInterface iface) { - if (iface.get_type() == typeof(Adapter1)) { - Adapter1 adapter = iface as Adapter1; - add_adapter(adapter); - } else if (iface.get_type() == typeof(Device1)) { - Device1 device = iface as Device1; - add_device(device); - } - } - - private void interface_removed(DBusObject object, DBusInterface iface) { - if (iface.get_type() == typeof(Adapter1)) { - adapter_removed(object.get_object_path()); - } else if (iface.get_type() == typeof(Device1)) { - queue_remove_device(object.get_object_path()); - } - } - - private void object_added(DBusObject object) { - var ifaces = object.get_interfaces(); - foreach (var iface in ifaces) { - interface_added(object, iface); - } - } - - private void object_removed(DBusObject object) { - var ifaces = object.get_interfaces(); - foreach (var iface in ifaces) { - interface_removed(object, iface); - } - } - - private List? filter_adapter_list(List object_list) { - List ret = null; - - foreach (var object in object_list) { - var iface = object.get_interface(BLUEZ_ADAPTER_INTERFACE); - if (iface != null) ret.append(iface); - } - - return ret; - } - - private void make_client_cb(Object? obj, AsyncResult? res) { - try { - dbus_object_manager = make_dbus_object_manager.end(res); - } catch (Error e) { - if (!e.matches(DBusError.IO_ERROR, IOError.CANCELLED)) { - critical("error getting DBusObjectManager for Bluez: %s", e.message); - } - return; - } - - // Connect manager signals - dbus_object_manager.interface_added.connect(interface_added); - dbus_object_manager.interface_removed.connect(interface_removed); - - dbus_object_manager.object_added.connect(object_added); - dbus_object_manager.object_removed.connect(object_removed); - - // Create the adapter list - var object_list = dbus_object_manager.get_objects(); - var adapter_list = filter_adapter_list(object_list); - - // Reverse sort the adapter list - adapter_list.sort((a, b) => { - DBusProxy adapter_a = a as DBusProxy; - DBusProxy adapter_b = b as DBusProxy; - - return adapter_b.get_object_path().collate(adapter_a.get_object_path()); - }); - - // Add all of the adapters - debug("Adding adapters from DBus Object Manager"); - foreach (var adapter in adapter_list) { - add_adapter(adapter as Adapter1); + private void on_interface_removed(DBusObject object, DBusInterface iface) { + if (iface is Adapter1) { + // FIXME: GLib.List has an is_empty() function, but for some reason it's not found + // when used in this subdir. + has_adapter = get_adapters().length() > 0; + } else if (iface is Device1) { + device_removed(iface as Device1); } } @@ -829,26 +278,32 @@ class BluetoothClient : GLib.Object { */ private void upower_device_added_cb(Device up_device) { var serial = up_device.serial; + message("upower_device_added_cb"); // Make sure the device has a valid Bluetooth address if (serial == null || !is_valid_address(serial)) { return; } - var device = get_device_for_address(serial); + // Get the device with the address + string? key = null; + Device? value = null; + var found = upower_devices.lookup_extended(up_device.get_object_path(), out key, out value); + if (found) message("Key in HashTable found: %s", key); + else message("Key not found. Sadge :("); - if (device == null) { - warning("Could not find Bluetooth device for UPower device with serial '%s'", serial); - return; - } + // if (device == null) { + // warning("Could not find Bluetooth device for UPower device with serial '%s'", serial); + // return; + // } - // Connect signals - up_device.notify["battery-level"].connect(() => device.update_battery(up_device)); - up_device.notify["percentage"].connect(() => device.update_battery(up_device)); + // // Connect signals + // up_device.notify["battery-level"].connect(() => device.update_battery(up_device)); + // up_device.notify["percentage"].connect(() => device.update_battery(up_device)); - // Update the power properties - device.set_upower_device(up_device); - device.update_battery(up_device); + // // Update the power properties + // device.set_upower_device(up_device); + // device.update_battery(up_device); } /** @@ -858,19 +313,19 @@ class BluetoothClient : GLib.Object { * association removed, and its battery properties reset. */ private void upower_device_removed_cb(string object_path) { - var device = get_device_for_upower_device(object_path); + // var device = get_device_with_object_path(object_path); - if (device == null) { - return; - } + // if (device == null) { + // return; + // } - debug("Removing Upower Device '%s' for Bluetooth device '%s'", object_path, device.get_object_path()); + // debug("Removing Upower Device '%s' for Bluetooth device '%s'", object_path, device.get_object_path()); - // Reset device power properties - device.set_upower_device(null); - device.battery_type = BatteryType.NONE; - device.battery_level = DeviceLevel.NONE; - device.battery_percentage = 0.0f; + // // Reset device power properties + // device.set_upower_device(null); + // device.battery_type = BatteryType.NONE; + // device.battery_level = DeviceLevel.NONE; + // device.battery_percentage = 0.0f; } /** @@ -914,23 +369,6 @@ class BluetoothClient : GLib.Object { upower_client.get_devices_async.begin(cancellable, upower_get_devices_cb); } - private void make_upower_cb(Object? obj, AsyncResult? res) { - try { - upower_client = make_upower_client.end(res); - } catch (Error e) { - critical("Error creating UPower client: %s", e.message); - return; - } - - upower_client.device_added.connect(upower_device_added_cb); - upower_client.device_removed.connect(upower_device_removed_cb); - - // Maybe coldplug UPower devices - if (bluez_devices_coldplugged) { - coldplug_client(); - } - } - /** * Gets the type of Bluetooth device based on its appearance value. * This is usually found in the GAP service. @@ -1082,89 +520,120 @@ class BluetoothClient : GLib.Object { } /** - * Returns a ListStore representing the devices connected to the current adapter. + * Get all Bluetooth adapters from our Bluez object manager. */ - public ListStore get_devices() { - return list_store; + public List get_adapters() { + var adapters = new List(); + + object_manager.get_objects().foreach((object) => { + var iface = object.get_interface(BLUEZ_ADAPTER_INTERFACE); + if (iface == null) return; + adapters.append(iface as Adapter1); + }); + + return (owned) adapters; } /** - * Starts pairing a Bluetooth device. + * Get all Bluetooth devices from our Bluez object manager. */ - public async void setup_device(string path) throws DBusError, IOError { - var device = get_device_for_path(path); + public List get_devices() { + var devices = new List(); - if (device == null) return; - - var proxy = device.proxy; - var device1 = proxy as Device1; + object_manager.get_objects().foreach((object) => { + var iface = object.get_interface(BLUEZ_DEVICE_INTERFACE); + if (iface == null) return; + devices.append(iface as Device1); + }); - yield device1.Pair(); + return (owned) devices; } /** - * Cancels pairing a Bluetooth device. + * Check if any adapter is currently connected. */ - public async void cancel_setup_device(string path) throws DBusError, IOError { - var device = get_device_for_path(path); + public bool get_connected() { + var devices = get_devices(); - if (device == null) return; - - var proxy = device.proxy; - var device1 = proxy as Device1; + foreach (var device in devices) { + if (device.Connected) return true; + } - yield device1.CancelPairing(); + return false; } /** - * Sets whether or not a Bluetooth device is trusted. + * Check if any adapter is powered on. */ - public void set_trusted(string path, bool trusted) { - var device = get_device_for_path(path); + public bool get_powered() { + var adapters = get_adapters(); - if (device == null) return; + foreach (var adapter in adapters) { + if (adapter.Powered) return true; + } - device.trusted = trusted; + return false; } /** - * Starts connecting to one of the known-connectable services on a device. + * Check if any Bluetooth adapter is powered and connected, and update our + * Bluetooth state accordingly. */ - public async void connect_service(string path, bool connect) throws DBusError, IOError { - var device = get_device_for_path(path); + public void check_powered() { + // This is called usually as a signal handler, so start an Idle + // task to prevent race conditions. + Idle.add(() => { + // Get current state + var connected = get_connected(); + var powered = get_powered(); - if (device == null) return; + // Do nothing if the state hasn't changed + if (connected == is_connected && powered == is_powered) return Source.REMOVE; - var proxy = device.proxy; - var device1 = proxy as Device1; + // Set the new state + is_connected = connected; + is_powered = powered; - if (connect) { - yield device1.Connect(); - } else { - yield device1.Disconnect(); - } + // Emit changed signal + global_state_changed(powered, connected); + + return Source.REMOVE; + }); } /** - * Returns whether or not there are Bluetooth devices connected that have input capabilities. + * Set the powered state of all adapters. If being powered off and an adapter has + * devices connected to it, they will be disconnected. + * + * It is intended to use `check_powered()` as a callback to this async function. + * As such, this function does not set our global state directly. */ - public bool has_connected_input_devices() { - var connected = false; - var num_items = list_store.get_n_items(); + public async void set_all_powered(bool powered) { + // Set the adapters' powered state + var adapters = get_adapters(); + foreach (var adapter in adapters) { + adapter.Powered = powered; + } - for (var i = 0; i < num_items; i++) { - var obj = list_store.get_item(i); - var device = obj as BluetoothDevice; + is_enabled = powered; - if (!device.connected) continue; - if (device.uuids == null || device.uuids.length == 0) continue; + if (powered) return; - if ("Human Interface Device" in device.uuids || "HumanInterfaceDeviceService" in device.uuids) { - connected = true; - break; + // If the power is being turned off, disconnect from all devices + var devices = get_devices(); + foreach (var device in devices) { + if (device.Connected) { + try { + yield device.Disconnect(); + } catch (Error e) { + warning("Error disconnecting Bluetooth device: %s", e.message); + } } } + } - return connected; + public async void set_last_powered() { + yield set_all_powered(is_enabled); + check_powered(); } } diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 7365ac7d2..e5576b016 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -63,12 +63,12 @@ public class BluetoothIndicator : Gtk.Bin { // Create our Bluetooth client client = new BluetoothClient(); - client.device_added.connect((path) => { - message("Bluetooth device added: %s", path); + client.device_added.connect((device) => { + message("Bluetooth device added: %s", device.Alias); }); - client.device_removed.connect((path) => { - message("Bluetooth device removed: %s", path); + client.device_removed.connect((device) => { + message("Bluetooth device removed: %s", device.Alias); }); add(ebox); From 588dbabcae6f787c60eaea2471c84a58c806a742 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Thu, 19 Jan 2023 17:26:30 -0500 Subject: [PATCH 08/81] Follow Vala naming conventions It'll automagically figure it out for DBus. Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 22 +++--- src/panel/applets/status/BluetoothDBus.vala | 76 +++++++++---------- src/panel/applets/status/BluetoothDevice.vala | 14 ++-- .../applets/status/BluetoothIndicator.vala | 4 +- 4 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index b178d7e09..f502f9fe3 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -196,17 +196,17 @@ class BluetoothClient : GLib.Object { */ private void get_type_and_icon_for_device(Device1 device, out BluetoothType type, out string icon) { // Special case these joypads - if (device.Name == "ION iCade Game Controller" || device.Name == "8Bitdo Zero GamePad") { + if (device.name == "ION iCade Game Controller" || device.name == "8Bitdo Zero GamePad") { type = BluetoothType.JOYPAD; icon = "input-gaming"; return; } // First, try to match the appearance of the device - type = appearance_to_type(device.Appearance); + type = appearance_to_type(device.appearance); // Match on the class if the appearance failed if (type == BluetoothType.ANY) { - type = class_to_type(device.Class); + type = class_to_type(device.class); } // Try to get an icon now @@ -214,7 +214,7 @@ class BluetoothClient : GLib.Object { // Fallback to the device's specified icon if (icon == null) { - icon = device.Icon; + icon = device.icon; } // Fallback to a generic icon @@ -238,7 +238,7 @@ class BluetoothClient : GLib.Object { } else if (iface is Device1) { unowned Device1 device = iface as Device1; - if (device.Paired) device_added(device); + if (device.paired) device_added(device); ((DBusProxy) device).g_properties_changed.connect((changed, invalid) => { var connected = changed.lookup_value("Connected", new VariantType("b")); @@ -250,7 +250,7 @@ class BluetoothClient : GLib.Object { if (paired == null) return; // Add or remove the device if it is paired or not - if (device.Paired) { + if (device.paired) { upower_devices[((DBusProxy) device).get_object_path()] = null; device_added(device); } else device_removed(device); @@ -556,7 +556,7 @@ class BluetoothClient : GLib.Object { var devices = get_devices(); foreach (var device in devices) { - if (device.Connected) return true; + if (device.connected) return true; } return false; @@ -569,7 +569,7 @@ class BluetoothClient : GLib.Object { var adapters = get_adapters(); foreach (var adapter in adapters) { - if (adapter.Powered) return true; + if (adapter.powered) return true; } return false; @@ -612,7 +612,7 @@ class BluetoothClient : GLib.Object { // Set the adapters' powered state var adapters = get_adapters(); foreach (var adapter in adapters) { - adapter.Powered = powered; + adapter.powered = powered; } is_enabled = powered; @@ -622,9 +622,9 @@ class BluetoothClient : GLib.Object { // If the power is being turned off, disconnect from all devices var devices = get_devices(); foreach (var device in devices) { - if (device.Connected) { + if (device.connected) { try { - yield device.Disconnect(); + yield device.disconnect(); } catch (Error e) { warning("Error disconnecting Bluetooth device: %s", e.message); } diff --git a/src/panel/applets/status/BluetoothDBus.vala b/src/panel/applets/status/BluetoothDBus.vala index 28b0228ca..427efcfd6 100644 --- a/src/panel/applets/status/BluetoothDBus.vala +++ b/src/panel/applets/status/BluetoothDBus.vala @@ -15,24 +15,24 @@ */ [DBus (name="org.bluez.Adapter1")] public interface Adapter1 : GLib.Object { - public abstract string Address { owned get; } - public abstract string Name { owned get; } - public abstract string Alias { owned get; set; } - public abstract uint32 Class { get; } - public abstract bool Powered { get; set; } - public abstract string PoweredState { owned get; } - public abstract bool Discoverable { set; get; } - public abstract uint32 DiscoverableTimeout { get; set; } - public abstract bool Pairable { get; set; } - public abstract uint32 PairableTimeout { get; set; } - public abstract bool Discovering { get; set; } + public abstract string address { owned get; } + public abstract string name { owned get; } + public abstract string alias { owned get; set; } + public abstract uint32 @class { get; } + public abstract bool powered { get; set; } + public abstract string powered_state { owned get; } + public abstract bool discoverable { set; get; } + public abstract uint32 discoverable_timeout { get; set; } + public abstract bool pairable { get; set; } + public abstract uint32 pairable_timeout { get; set; } + public abstract bool discovering { get; set; } public abstract string[] UUIDS { owned get; } - public abstract string Modalias { owned get; } + public abstract string modalias { owned get; } - public async abstract void StartDiscovery() throws GLib.DBusError, GLib.IOError; - public async abstract void StopDiscovery() throws GLib.DBusError, GLib.IOError; - public async abstract void RemoveDevice(GLib.ObjectPath device) throws GLib.DBusError, GLib.IOError; - public async abstract void SetDiscoveryFilter(HashTable properties) throws GLib.DBusError, GLib.IOError; + public async abstract void start_discovery() throws GLib.DBusError, GLib.IOError; + public async abstract void stop_discovery() throws GLib.DBusError, GLib.IOError; + public async abstract void remove_device(GLib.ObjectPath device) throws GLib.DBusError, GLib.IOError; + public async abstract void set_discovery_filter(HashTable properties) throws GLib.DBusError, GLib.IOError; } /** @@ -40,28 +40,28 @@ public interface Adapter1 : GLib.Object { */ [DBus (name = "org.bluez.Device1")] public interface Device1 : GLib.Object { - public abstract string Address { owned get; } - public abstract string Name { owned get; } - public abstract string Alias { owned get; set; } - public abstract uint32 Class { get; } - public abstract uint16 Appearance { get; } - public abstract string Icon { owned get; } - public abstract bool Paired { get; } - public abstract bool Trusted { get; set; } - public abstract bool Blocked { get; set; } - public abstract bool LegacyPairing { get; } + public abstract string address { owned get; } + public abstract string name { owned get; } + public abstract string alias { owned get; set; } + public abstract uint32 @class { get; } + public abstract uint16 appearance { get; } + public abstract string icon { owned get; } + public abstract bool paired { get; } + public abstract bool trusted { get; set; } + public abstract bool blocked { get; set; } + public abstract bool legacy_pairing { get; } public abstract int16 RSSI { get; } - public abstract bool Connected { get; } + public abstract bool connected { get; } public abstract string[] UUIDs { owned get; } - public abstract string Modalias { owned get; } - public abstract GLib.ObjectPath Adapter { owned get; } + public abstract string modalias { owned get; } + public abstract GLib.ObjectPath adapter { owned get; } - public async abstract void Connect() throws GLib.DBusError, GLib.IOError; - public async abstract void Disconnect() throws GLib.DBusError, GLib.IOError; - public async abstract void ConnectProfile(string uuid) throws GLib.DBusError, GLib.IOError; - public async abstract void DisconnectProfile(string uuid) throws GLib.DBusError, GLib.IOError; - public async abstract void Pair() throws GLib.DBusError, GLib.IOError; - public async abstract void CancelPairing() throws GLib.DBusError, GLib.IOError; + public async abstract void connect() throws GLib.DBusError, GLib.IOError; + public async abstract void disconnect() throws GLib.DBusError, GLib.IOError; + public async abstract void connect_profile(string uuid) throws GLib.DBusError, GLib.IOError; + public async abstract void disconnect_profile(string uuid) throws GLib.DBusError, GLib.IOError; + public async abstract void pair() throws GLib.DBusError, GLib.IOError; + public async abstract void cancel_pairing() throws GLib.DBusError, GLib.IOError; } /** @@ -69,7 +69,7 @@ public interface Device1 : GLib.Object { */ [DBus (name = "org.bluez.AgentManager1")] public interface AgentManager1 : GLib.Object { - public async abstract void RegisterAgent(GLib.ObjectPath agent, string capability) throws GLib.DBusError, GLib.IOError; - public async abstract void UnregisterAgent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; - public async abstract void RequestDefaultAgent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; + public async abstract void register_agent(GLib.ObjectPath agent, string capability) throws GLib.DBusError, GLib.IOError; + public async abstract void unregister_agent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; + public async abstract void request_default_agent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; } diff --git a/src/panel/applets/status/BluetoothDevice.vala b/src/panel/applets/status/BluetoothDevice.vala index 332bc16e4..5f1dfa7f9 100644 --- a/src/panel/applets/status/BluetoothDevice.vala +++ b/src/panel/applets/status/BluetoothDevice.vala @@ -39,16 +39,16 @@ public class BluetoothDevice : Object { public BluetoothDevice(Device1 device, BluetoothType type, string icon) { Object( proxy: device as DBusProxy, - address: device.Address, - alias: device.Alias, - name: device.Name, + address: device.address, + alias: device.alias, + name: device.name, device_type: type, icon: icon, - legacy_pairing: device.LegacyPairing, + legacy_pairing: device.legacy_pairing, uuids: device.UUIDs, - paired: device.Paired, - connected: device.Connected, - trusted: device.Trusted + paired: device.paired, + connected: device.connected, + trusted: device.trusted ); } diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index e5576b016..5ccbb473c 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -64,11 +64,11 @@ public class BluetoothIndicator : Gtk.Bin { // Create our Bluetooth client client = new BluetoothClient(); client.device_added.connect((device) => { - message("Bluetooth device added: %s", device.Alias); + message("Bluetooth device added: %s", device.alias); }); client.device_removed.connect((device) => { - message("Bluetooth device removed: %s", device.Alias); + message("Bluetooth device removed: %s", device.alias); }); add(ebox); From 72b4082068e783477a7895616f69578a953fdd0a Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Fri, 20 Jan 2023 11:34:52 -0500 Subject: [PATCH 09/81] Add new Bluetooth popover layout Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 7 + .../applets/status/BluetoothIndicator.vala | 123 +++++++++++++----- 2 files changed, 94 insertions(+), 36 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index f502f9fe3..2fafe065d 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -257,6 +257,8 @@ class BluetoothClient : GLib.Object { check_powered(); }); + + check_powered(); } } @@ -587,6 +589,11 @@ class BluetoothClient : GLib.Object { var connected = get_connected(); var powered = get_powered(); + debug("connected: %s new_connected: %s | powered: %s new_powered: %s", + is_connected ? "yes" : "no", connected ? "yes" : "no", + is_powered ? "yes" : "no", powered ? "yes" : "no" + ); + // Do nothing if the state hasn't changed if (connected == is_connected && powered == is_powered) return Source.REMOVE; diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 5ccbb473c..5cac606d6 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -12,54 +12,78 @@ * BluetoothIndicator is largely inspired by gnome-flashback. */ -public class BluetoothIndicator : Gtk.Bin { - public Gtk.Image? image = null; - public Gtk.EventBox? ebox = null; +using Gdk; +using Gtk; + +public class BluetoothIndicator : Bin { + public Image? image = null; + public EventBox? ebox = null; public Budgie.Popover? popover = null; - private Gtk.CheckButton radio_airplane; - private ulong radio_id; - private Gtk.Button send_to; + private ListBox? devices_box = null; + private Stack? stack = null; + private Switch? bluetooth_switch = null; private BluetoothClient client; public BluetoothIndicator() { - image = new Gtk.Image.from_icon_name("bluetooth-active-symbolic", Gtk.IconSize.MENU); + image = new Image.from_icon_name("bluetooth-active-symbolic", IconSize.MENU); - ebox = new Gtk.EventBox(); + ebox = new EventBox(); ebox.add(image); - ebox.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK); + ebox.add_events(EventMask.BUTTON_RELEASE_MASK); + ebox.button_release_event.connect(on_button_released); // Create our popover popover = new Budgie.Popover(ebox); - var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 1); - box.border_width = 6; - popover.add(box); - // Settings button - var button = new Gtk.Button.with_label(_("Bluetooth Settings")); - button.get_child().set_halign(Gtk.Align.START); - button.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); - button.clicked.connect(on_settings_activate); - box.pack_start(button, false, false, 0); + // Create our stack + stack = new Stack() { + border_width = 6, + hhomogeneous = true, + transition_duration = 250, + transition_type = SLIDE_LEFT_RIGHT + }; - // Send files button - send_to = new Gtk.Button.with_label(_("Send Files")); - send_to.get_child().set_halign(Gtk.Align.START); - send_to.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); - // send_to.clicked.connect(on_send_file); - box.pack_start(send_to, false, false, 0); + // Create the main stack page + var main_page = new Box(VERTICAL, 0); + stack.add_named(main_page, "main"); - var sep = new Gtk.Separator(Gtk.Orientation.HORIZONTAL); - box.pack_start(sep, false, false, 1); + // Main header + var main_header = new Box(HORIZONTAL, 0); - // Airplane mode - radio_airplane = new Gtk.CheckButton.with_label(_("Bluetooth Airplane Mode")); - radio_airplane.get_child().set_property("margin", 4); - box.pack_start(radio_airplane, false, false, 0); + // Header label + var switch_label = new Label(_("Bluetooth")); + switch_label.get_style_context().add_class("dim-label"); + main_header.pack_start(switch_label); - // Ensure all content is shown - box.show_all(); + // Settings button + var button = new Button.from_icon_name("preferences-system-symbolic", MENU) { + tooltip_text = _("Bluetooth Settings") + }; + button.get_style_context().add_class(STYLE_CLASS_FLAT); + button.clicked.connect(on_settings_activate); + main_header.pack_end(button, false, false, 0); + + // Bluetooth switch + bluetooth_switch = new Switch() { + tooltip_text = _("Turn Bluetooth on or off") + }; + bluetooth_switch.notify["active"].connect(on_switch_activate); + main_header.pack_end(bluetooth_switch); + + main_page.pack_start(main_header); + main_page.pack_start(new Separator(HORIZONTAL), false, false, 1); + + // Devices + var scrolled_window = new ScrolledWindow(null, null) { + hscrollbar_policy = NEVER, + min_content_height = 250, + max_content_height = 250 + }; + devices_box = new ListBox(); + scrolled_window.add(devices_box); + main_page.pack_start(scrolled_window); // Create our Bluetooth client client = new BluetoothClient(); @@ -71,21 +95,48 @@ public class BluetoothIndicator : Gtk.Bin { message("Bluetooth device removed: %s", device.alias); }); + client.global_state_changed.connect(on_client_state_changed); + + // Pack and show add(ebox); + popover.add(stack); + stack.show_all(); + stack.set_visible_child_name("main"); show_all(); } - void on_settings_activate() { + private bool on_button_released(EventButton e) { + if (e.button != BUTTON_MIDDLE) return EVENT_PROPAGATE; + + // Disconnect all Bluetooth on middle click + client.set_all_powered.begin(!client.get_powered(), (obj, res) => { + client.check_powered(); + }); + + return Gdk.EVENT_STOP; + } + + private void on_client_state_changed(bool enabled, bool connected) { + bluetooth_switch.active = enabled; + } + + private void on_settings_activate() { this.popover.hide(); var app_info = new DesktopAppInfo("budgie-bluetooth-panel.desktop"); - if (app_info == null) { - return; - } + if (app_info == null) return; + try { app_info.launch(null, null); } catch (Error e) { message("Unable to launch budgie-bluetooth-panel.desktop: %s", e.message); } } + + private void on_switch_activate() { + // Turn Bluetooth on or off + client.set_all_powered.begin(bluetooth_switch.active, (obj, res) => { + client.check_powered(); + }); + } } From ae5f3531961abbeab6aa18c43ddf85ae8de2bed7 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 21 Jan 2023 13:40:51 -0500 Subject: [PATCH 10/81] Add widgets for Bluetooth devices Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 19 +- src/panel/applets/status/BluetoothDBus.vala | 16 +- .../applets/status/BluetoothIndicator.vala | 197 +++++++++++++++++- 3 files changed, 203 insertions(+), 29 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 2fafe065d..3e2ed9a37 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -194,7 +194,7 @@ class BluetoothClient : GLib.Object { /** * Get the type of a Bluetooth device, and use that type to get an icon for it. */ - private void get_type_and_icon_for_device(Device1 device, out BluetoothType type, out string icon) { + public void get_type_and_icon_for_device(Device1 device, out BluetoothType type, out string icon) { // Special case these joypads if (device.name == "ION iCade Game Controller" || device.name == "8Bitdo Zero GamePad") { type = BluetoothType.JOYPAD; @@ -237,24 +237,9 @@ class BluetoothClient : GLib.Object { }); } else if (iface is Device1) { unowned Device1 device = iface as Device1; - - if (device.paired) device_added(device); + device_added(device); ((DBusProxy) device).g_properties_changed.connect((changed, invalid) => { - var connected = changed.lookup_value("Connected", new VariantType("b")); - if (connected != null) { - check_powered(); - } - - var paired = changed.lookup_value("Paired", new VariantType("b")); - if (paired == null) return; - - // Add or remove the device if it is paired or not - if (device.paired) { - upower_devices[((DBusProxy) device).get_object_path()] = null; - device_added(device); - } else device_removed(device); - check_powered(); }); diff --git a/src/panel/applets/status/BluetoothDBus.vala b/src/panel/applets/status/BluetoothDBus.vala index 427efcfd6..8ce738c25 100644 --- a/src/panel/applets/status/BluetoothDBus.vala +++ b/src/panel/applets/status/BluetoothDBus.vala @@ -43,15 +43,15 @@ public interface Device1 : GLib.Object { public abstract string address { owned get; } public abstract string name { owned get; } public abstract string alias { owned get; set; } - public abstract uint32 @class { get; } - public abstract uint16 appearance { get; } + public abstract uint32 @class { owned get; } + public abstract uint16 appearance { owned get; } public abstract string icon { owned get; } - public abstract bool paired { get; } - public abstract bool trusted { get; set; } - public abstract bool blocked { get; set; } - public abstract bool legacy_pairing { get; } - public abstract int16 RSSI { get; } - public abstract bool connected { get; } + public abstract bool paired { owned get; } + public abstract bool trusted { owned get; set; } + public abstract bool blocked { owned get; set; } + public abstract bool legacy_pairing { owned get; } + public abstract int16 RSSI { owned get; } + public abstract bool connected { owned get; } public abstract string[] UUIDs { owned get; } public abstract string modalias { owned get; } public abstract GLib.ObjectPath adapter { owned get; } diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 5cac606d6..e48a0ea00 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -26,7 +26,9 @@ public class BluetoothIndicator : Bin { private BluetoothClient client; - public BluetoothIndicator() { + construct { + get_style_context().add_class("bluetooth-applet-popover"); + image = new Image.from_icon_name("bluetooth-active-symbolic", IconSize.MENU); ebox = new EventBox(); @@ -51,6 +53,7 @@ public class BluetoothIndicator : Bin { // Main header var main_header = new Box(HORIZONTAL, 0); + main_header.get_style_context().add_class("bluetooth-applet-header"); // Header label var switch_label = new Label(_("Bluetooth")); @@ -81,18 +84,32 @@ public class BluetoothIndicator : Bin { min_content_height = 250, max_content_height = 250 }; - devices_box = new ListBox(); + devices_box = new ListBox() { + selection_mode = NONE + }; + devices_box.set_sort_func(sort_devices); + devices_box.get_style_context().add_class("bluetooth-devices-listbox"); + + devices_box.row_activated.connect((row) => { + var widget = row.get_child() as BluetoothDeviceWidget; + widget.toggle_revealer(); + }); + scrolled_window.add(devices_box); main_page.pack_start(scrolled_window); // Create our Bluetooth client client = new BluetoothClient(); + client.device_added.connect((device) => { - message("Bluetooth device added: %s", device.alias); + // Remove any existing rows for this device + remove_device(device); + // Add the new device to correctly update its status + add_device(device); }); client.device_removed.connect((device) => { - message("Bluetooth device removed: %s", device.alias); + remove_device(device); }); client.global_state_changed.connect(on_client_state_changed); @@ -139,4 +156,176 @@ public class BluetoothIndicator : Bin { client.check_powered(); }); } + + private void add_device(Device1 device) { + debug("Bluetooth device added: %s", device.alias); + + BluetoothType type = 0; + string? icon = null; + client.get_type_and_icon_for_device(device, out type, out icon); + + var device_obj = new BluetoothDevice(device, type, icon); + var widget = new BluetoothDeviceWidget(device_obj); + + widget.properties_updated.connect(() => { + client.check_powered(); + devices_box.invalidate_sort(); + }); + + devices_box.add(widget); + devices_box.invalidate_sort(); + } + + private void remove_device(Device1 device) { + debug("Bluetooth device removed: %s", device.alias); + + devices_box.foreach((row) => { + var child = ((ListBoxRow) row).get_child() as BluetoothDeviceWidget; + var proxy = child.device.proxy as Device1; + if (proxy.address == device.address) { + row.destroy(); + } + }); + + devices_box.invalidate_sort(); + } + + /** + * Sorts items based on their names and connection status. + * + * Items are sorted alphabetically, with connected devices at the top of the list. + */ + private int sort_devices(ListBoxRow a, ListBoxRow b) { + var a_device = a.get_child() as BluetoothDeviceWidget; + var b_device = b.get_child() as BluetoothDeviceWidget; + + if (((Device1) a_device.device.proxy).connected && ((Device1) b_device.device.proxy).connected) return strcmp(a_device.device.alias, b_device.device.alias); + else if (((Device1) a_device.device.proxy).connected) return -1; // A should go before B + else if (((Device1) b_device.device.proxy).connected) return 1; // B should go before A + else return strcmp(a_device.device.alias, b_device.device.alias); + } +} + +public class BluetoothDeviceWidget : Box { + private Image? image = null; + private Label? name_label = null; + private Label? status_label = null; + private Revealer? revealer = null; + private Button? connection_button = null; + + public BluetoothDevice device { get; construct; } + + public signal void properties_updated(); + + construct { + get_style_context().add_class("bluetooth-widget"); + + // Body + var grid = new Grid(); + + image = new Image.from_icon_name(device.icon, LARGE_TOOLBAR) { + halign = START, + margin_end = 6 + }; + + name_label = new Label(device.alias) { + valign = CENTER, + xalign = 0.0f, + max_width_chars = 1, + ellipsize = END, + hexpand = true, + tooltip_text = device.alias + }; + + status_label = new Label(null) { + halign = START, + hexpand = true + }; + status_label.get_style_context().add_class("dim-label"); + + // Revealer stuff + revealer = new Revealer() { + reveal_child = false, + transition_duration = 250, + transition_type = RevealerTransitionType.SLIDE_DOWN + }; + revealer.get_style_context().add_class("bluetooth-widget-revealer"); + + var revealer_body = new Box(HORIZONTAL, 0); + connection_button = new Button.with_label(""); + connection_button.clicked.connect(on_connection_button_clicked); + + revealer_body.pack_start(connection_button); + revealer.add(revealer_body); + + // Signals + device.proxy.g_properties_changed.connect(update_status); + + // Packing + grid.attach(image, 0, 0); + grid.attach(name_label, 1, 0); + grid.attach(status_label, 1, 1); + + pack_start(grid); + pack_start(revealer); + + update_status(); + show_all(); + } + + public BluetoothDeviceWidget(BluetoothDevice device) { + Object( + device: device, + orientation: Orientation.VERTICAL, + spacing: 0 + ); + } + + public void toggle_revealer() { + revealer.reveal_child = !revealer.reveal_child; + } + + private void on_connection_button_clicked() { + connection_button.sensitive = false; + + if (((Device1) device.proxy).connected) { + ((Device1) device.proxy).disconnect.begin((obj, res) => { + try { + ((Device1) device.proxy).disconnect.end(res); + } catch (Error e) { + warning("Failed to disconnect Bluetooth device %s: %s", device.alias, e.message); + } + + connection_button.sensitive = true; + }); + } else { + ((Device1) device.proxy).connect.begin((obj, res) => { + try { + ((Device1) device.proxy).connect.end(res); + } catch (Error e) { + warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); + } + + connection_button.sensitive = true; + }); + } + } + + private void update_status() { + if (((Device1) device.proxy).connected) { + status_label.set_text(_("Connected")); + connection_button.label = _("Disconnect"); + } else { + status_label.set_text(_("Disconnected")); + connection_button.label = _("Connect"); + } + + // Device isn't paired + if (!((Device1) device.proxy).paired) { + status_label.set_text(_("Not paired")); + connection_button.sensitive = false; + } + + properties_updated(); + } } From 1d0a8fe0b859be27656896e6d4bfb9622c8d516a Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 21 Jan 2023 13:45:01 -0500 Subject: [PATCH 11/81] Pack popover header like the new network popover header Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index e48a0ea00..5d02195c8 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -58,7 +58,6 @@ public class BluetoothIndicator : Bin { // Header label var switch_label = new Label(_("Bluetooth")); switch_label.get_style_context().add_class("dim-label"); - main_header.pack_start(switch_label); // Settings button var button = new Button.from_icon_name("preferences-system-symbolic", MENU) { @@ -66,14 +65,16 @@ public class BluetoothIndicator : Bin { }; button.get_style_context().add_class(STYLE_CLASS_FLAT); button.clicked.connect(on_settings_activate); - main_header.pack_end(button, false, false, 0); // Bluetooth switch bluetooth_switch = new Switch() { tooltip_text = _("Turn Bluetooth on or off") }; bluetooth_switch.notify["active"].connect(on_switch_activate); + + main_header.pack_start(switch_label); main_header.pack_end(bluetooth_switch); + main_header.pack_end(button, false, false, 0); main_page.pack_start(main_header); main_page.pack_start(new Separator(HORIZONTAL), false, false, 1); From a098229a90f74c8aa0e05441d7f87026fd695a2e Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 21 Jan 2023 16:34:56 -0500 Subject: [PATCH 12/81] Remove device wrapper class Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 195 ------------------ src/panel/applets/status/BluetoothDevice.vala | 96 --------- .../applets/status/BluetoothIndicator.vala | 38 ++-- src/panel/applets/status/meson.build | 1 - 4 files changed, 16 insertions(+), 314 deletions(-) delete mode 100644 src/panel/applets/status/BluetoothDevice.vala diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 3e2ed9a37..93d8f232c 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -154,75 +154,6 @@ class BluetoothClient : GLib.Object { // return null; // } - /** - * Tries to get an icon name present in GTK themes for a Bluetooth type. - * - * Not all types have relevant icons. Any type that doesn't have an icon - * will return `null`. - */ - private string? get_icon_for_type(BluetoothType type) { - switch (type) { - case COMPUTER: - return "computer"; - case HEADSET: - return "audio-headset"; - case HEADPHONES: - return "audio-headphones"; - case KEYBOARD: - return "input-keyboard"; - case MOUSE: - return "input-mouse"; - case PRINTER: - return "printer"; - case JOYPAD: - return "input-gaming"; - case TABLET: - return "input-tablet"; - case SPEAKERS: - return "audio-speakers"; - case PHONE: - return "phone"; - case DISPLAY: - return "video-display"; - case SCANNER: - return "scanner"; - default: - return null; - } - } - - /** - * Get the type of a Bluetooth device, and use that type to get an icon for it. - */ - public void get_type_and_icon_for_device(Device1 device, out BluetoothType type, out string icon) { - // Special case these joypads - if (device.name == "ION iCade Game Controller" || device.name == "8Bitdo Zero GamePad") { - type = BluetoothType.JOYPAD; - icon = "input-gaming"; - return; - } - - // First, try to match the appearance of the device - type = appearance_to_type(device.appearance); - // Match on the class if the appearance failed - if (type == BluetoothType.ANY) { - type = class_to_type(device.class); - } - - // Try to get an icon now - icon = get_icon_for_type(type); - - // Fallback to the device's specified icon - if (icon == null) { - icon = device.icon; - } - - // Fallback to a generic icon - if (icon == null) { - icon = "bluetooth"; - } - } - /** * Handles the addition of a DBus object interface. */ @@ -356,132 +287,6 @@ class BluetoothClient : GLib.Object { upower_client.get_devices_async.begin(cancellable, upower_get_devices_cb); } - /** - * Gets the type of Bluetooth device based on its appearance value. - * This is usually found in the GAP service. - */ - private BluetoothType appearance_to_type(uint16 appearance) { - switch ((appearance & 0xffc0) >> 6) { - case 0x01: - return PHONE; - case 0x02: - return COMPUTER; - case 0x05: - return DISPLAY; - case 0x0a: - return OTHER_AUDIO; - case 0x0b: - return SCANNER; - case 0x0f: /* HID Generic */ - switch (appearance & 0x3f) { - case 0x01: - return KEYBOARD; - case 0x02: - return MOUSE; - case 0x03: - case 0x04: - return JOYPAD; - case 0x05: - return TABLET; - case 0x08: - return SCANNER; - } - break; - case 0x21: - return SPEAKERS; - case 0x25: /* Audio */ - switch (appearance & 0x3f) { - case 0x01: - case 0x02: - case 0x04: - return HEADSET; - case 0x03: - return HEADPHONES; - default: - return OTHER_AUDIO; - } - } - - return ANY; - } - - /** - * Gets the type of a Bluetooth device based on its class. - */ - private BluetoothType class_to_type(uint32 klass) { - switch ((klass & 0x1f00) >> 8) { - case 0x01: - return COMPUTER; - case 0x02: - switch ((klass & 0xfc) >> 2) { - case 0x01: - case 0x02: - case 0x03: - case 0x05: - return PHONE; - case 0x04: - return MODEM; - } - break; - case 0x03: - return NETWORK; - case 0x04: - switch ((klass & 0xfc) >> 2) { - case 0x01: - case 0x02: - return HEADSET; - case 0x05: - return SPEAKERS; - case 0x06: - return HEADPHONES; - case 0x0b: /* VCR */ - case 0x0c: /* Video Camera */ - case 0x0d: /* Camcorder */ - return VIDEO; - default: - return OTHER_AUDIO; - } - case 0x05: - switch ((klass & 0xc0) >> 6) { - case 0x00: - switch ((klass & 0x1e) >> 2) { - case 0x01: - case 0x02: - return JOYPAD; - case 0x03: - return REMOTE_CONTROL; - } - break; - case 0x01: - return KEYBOARD; - case 0x02: - switch ((klass & 0x1e) >> 2) { - case 0x05: - return TABLET; - default: - return MOUSE; - } - } - break; - case 0x06: - if ((klass & 0x80) == 1) - return PRINTER; - if ((klass & 0x40) == 1) - return SCANNER; - if ((klass & 0x20) == 1) - return CAMERA; - if ((klass & 0x10) == 1) - return DISPLAY; - break; - case 0x07: - return WEARABLE; - case 0x08: - return TOY; - } - - return ANY; - } - /** * Check if a Bluetooth address is valid. */ diff --git a/src/panel/applets/status/BluetoothDevice.vala b/src/panel/applets/status/BluetoothDevice.vala deleted file mode 100644 index 5f1dfa7f9..000000000 --- a/src/panel/applets/status/BluetoothDevice.vala +++ /dev/null @@ -1,96 +0,0 @@ -/* - * This file is part of budgie-desktop - * - * Copyright Budgie Desktop Developers - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - */ - -using GLib; -using Up; - -/** - * Wrapper for a Bluetooth device. - */ -public class BluetoothDevice : Object { - public DBusProxy proxy { get; set; default = null; } - public string address { get; set; } - public string alias { get; set; } - public string name { get; set; } - public BluetoothType device_type { get; set; default = BluetoothType.ANY; } - public string icon { get; set; } - public bool paired { get; set; default = false; } - public bool trusted { get; set; default = false; } - public bool connected { get; set; default = false; } - public bool legacy_pairing { get; set; default = false; } - public string[] uuids { get; set; } - public bool connectable { get; set; default = false; } - public BatteryType battery_type { get; set; default = BatteryType.NONE; } - [IntegerType (min = 0, max = 100)] - public double battery_percentage { get; set; default = 0.0; } - public DeviceLevel battery_level { get; set; default = DeviceLevel.UNKNOWN; } - - /** - * Create a new Bluetooth device wrapper object. - */ - public BluetoothDevice(Device1 device, BluetoothType type, string icon) { - Object( - proxy: device as DBusProxy, - address: device.address, - alias: device.alias, - name: device.name, - device_type: type, - icon: icon, - legacy_pairing: device.legacy_pairing, - uuids: device.UUIDs, - paired: device.paired, - connected: device.connected, - trusted: device.trusted - ); - } - - /** - * Gets the object path for this Bluetooth device. - */ - public string? get_object_path() { - if (proxy == null) { - return null; - } - - return proxy.get_object_path(); - } - - /** - * Get the associated UPower device for this Bluetooth device. - */ - public Device get_upower_device() { - return get_data("up-device"); - } - - /** - * Set an association between this Bluetooth device and a UPower device. - */ - public void set_upower_device(Device? up_device) { - set_data_full("up-device", up_device != null ? up_device.ref() : null, unref); - } - - /** - * Updates battery levels from a UPower device. - */ - public void update_battery(Device up_device) { - BatteryType type; - - if (up_device.battery_level == DeviceLevel.NONE) { - type = BatteryType.PERCENTAGE; - } else { - type = BatteryType.COARSE; - } - - battery_type = type; - battery_level = (DeviceLevel) up_device.battery_level; - battery_percentage = up_device.percentage; - } -} diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 5d02195c8..a63c5a6b3 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -161,12 +161,7 @@ public class BluetoothIndicator : Bin { private void add_device(Device1 device) { debug("Bluetooth device added: %s", device.alias); - BluetoothType type = 0; - string? icon = null; - client.get_type_and_icon_for_device(device, out type, out icon); - - var device_obj = new BluetoothDevice(device, type, icon); - var widget = new BluetoothDeviceWidget(device_obj); + var widget = new BluetoothDeviceWidget(device); widget.properties_updated.connect(() => { client.check_powered(); @@ -182,8 +177,7 @@ public class BluetoothIndicator : Bin { devices_box.foreach((row) => { var child = ((ListBoxRow) row).get_child() as BluetoothDeviceWidget; - var proxy = child.device.proxy as Device1; - if (proxy.address == device.address) { + if (child.device.address == device.address) { row.destroy(); } }); @@ -200,9 +194,9 @@ public class BluetoothIndicator : Bin { var a_device = a.get_child() as BluetoothDeviceWidget; var b_device = b.get_child() as BluetoothDeviceWidget; - if (((Device1) a_device.device.proxy).connected && ((Device1) b_device.device.proxy).connected) return strcmp(a_device.device.alias, b_device.device.alias); - else if (((Device1) a_device.device.proxy).connected) return -1; // A should go before B - else if (((Device1) b_device.device.proxy).connected) return 1; // B should go before A + if (a_device.device.connected && b_device.device.connected) return strcmp(a_device.device.alias, b_device.device.alias); + else if (a_device.device.connected) return -1; // A should go before B + else if (b_device.device.connected) return 1; // B should go before A else return strcmp(a_device.device.alias, b_device.device.alias); } } @@ -214,7 +208,7 @@ public class BluetoothDeviceWidget : Box { private Revealer? revealer = null; private Button? connection_button = null; - public BluetoothDevice device { get; construct; } + public Device1 device { get; construct; } public signal void properties_updated(); @@ -224,7 +218,7 @@ public class BluetoothDeviceWidget : Box { // Body var grid = new Grid(); - image = new Image.from_icon_name(device.icon, LARGE_TOOLBAR) { + image = new Image.from_icon_name(device.icon ?? "bluetooth", LARGE_TOOLBAR) { halign = START, margin_end = 6 }; @@ -260,7 +254,7 @@ public class BluetoothDeviceWidget : Box { revealer.add(revealer_body); // Signals - device.proxy.g_properties_changed.connect(update_status); + ((DBusProxy) device).g_properties_changed.connect(update_status); // Packing grid.attach(image, 0, 0); @@ -274,7 +268,7 @@ public class BluetoothDeviceWidget : Box { show_all(); } - public BluetoothDeviceWidget(BluetoothDevice device) { + public BluetoothDeviceWidget(Device1 device) { Object( device: device, orientation: Orientation.VERTICAL, @@ -289,10 +283,10 @@ public class BluetoothDeviceWidget : Box { private void on_connection_button_clicked() { connection_button.sensitive = false; - if (((Device1) device.proxy).connected) { - ((Device1) device.proxy).disconnect.begin((obj, res) => { + if (device.connected) { + device.disconnect.begin((obj, res) => { try { - ((Device1) device.proxy).disconnect.end(res); + device.disconnect.end(res); } catch (Error e) { warning("Failed to disconnect Bluetooth device %s: %s", device.alias, e.message); } @@ -300,9 +294,9 @@ public class BluetoothDeviceWidget : Box { connection_button.sensitive = true; }); } else { - ((Device1) device.proxy).connect.begin((obj, res) => { + device.connect.begin((obj, res) => { try { - ((Device1) device.proxy).connect.end(res); + device.connect.end(res); } catch (Error e) { warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); } @@ -313,7 +307,7 @@ public class BluetoothDeviceWidget : Box { } private void update_status() { - if (((Device1) device.proxy).connected) { + if (device.connected) { status_label.set_text(_("Connected")); connection_button.label = _("Disconnect"); } else { @@ -322,7 +316,7 @@ public class BluetoothDeviceWidget : Box { } // Device isn't paired - if (!((Device1) device.proxy).paired) { + if (!device.paired) { status_label.set_text(_("Not paired")); connection_button.sensitive = false; } diff --git a/src/panel/applets/status/meson.build b/src/panel/applets/status/meson.build index c1a2b28f0..a1aa44cc4 100644 --- a/src/panel/applets/status/meson.build +++ b/src/panel/applets/status/meson.build @@ -21,7 +21,6 @@ applet_status_resources = gnome.compile_resources( applet_status_sources = [ 'BluetoothClient.vala', 'BluetoothDBus.vala', - 'BluetoothDevice.vala', 'BluetoothEnums.vala', 'BluetoothIndicator.vala', 'StatusApplet.vala', From 64ec42851cb6615c3a656b7b71fcc9fe8eb5c5e7 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 21 Jan 2023 16:56:17 -0500 Subject: [PATCH 13/81] Make sure connection button is clickable if the device is now paired Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index a63c5a6b3..5c7f81de3 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -318,9 +318,11 @@ public class BluetoothDeviceWidget : Box { // Device isn't paired if (!device.paired) { status_label.set_text(_("Not paired")); - connection_button.sensitive = false; } + // Only make the (dis)connect button clickable if the device is paired + connection_button.sensitive = device.paired; + properties_updated(); } } From 91fe9cdea030375b760ea4f4ef065c4fe949d25e Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 21 Jan 2023 19:31:28 -0500 Subject: [PATCH 14/81] Implement pairing and forgetting Bluetooth devices Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 194 +++++++++++++++--- 1 file changed, 160 insertions(+), 34 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 5c7f81de3..f45b20788 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -21,11 +21,13 @@ public class BluetoothIndicator : Bin { public Budgie.Popover? popover = null; private ListBox? devices_box = null; - private Stack? stack = null; private Switch? bluetooth_switch = null; + private Button? pairing_button = null; private BluetoothClient client; + public bool pairing { get; private set; default = false; } + construct { get_style_context().add_class("bluetooth-applet-popover"); @@ -38,22 +40,11 @@ public class BluetoothIndicator : Bin { // Create our popover popover = new Budgie.Popover(ebox); + var box = new Box(VERTICAL, 0); - // Create our stack - stack = new Stack() { - border_width = 6, - hhomogeneous = true, - transition_duration = 250, - transition_type = SLIDE_LEFT_RIGHT - }; - - // Create the main stack page - var main_page = new Box(VERTICAL, 0); - stack.add_named(main_page, "main"); - - // Main header - var main_header = new Box(HORIZONTAL, 0); - main_header.get_style_context().add_class("bluetooth-applet-header"); + // Header + var header = new Box(HORIZONTAL, 0); + header.get_style_context().add_class("bluetooth-applet-header"); // Header label var switch_label = new Label(_("Bluetooth")); @@ -72,12 +63,9 @@ public class BluetoothIndicator : Bin { }; bluetooth_switch.notify["active"].connect(on_switch_activate); - main_header.pack_start(switch_label); - main_header.pack_end(bluetooth_switch); - main_header.pack_end(button, false, false, 0); - - main_page.pack_start(main_header); - main_page.pack_start(new Separator(HORIZONTAL), false, false, 1); + header.pack_start(switch_label); + header.pack_end(bluetooth_switch); + header.pack_end(button, false, false, 0); // Devices var scrolled_window = new ScrolledWindow(null, null) { @@ -89,6 +77,7 @@ public class BluetoothIndicator : Bin { selection_mode = NONE }; devices_box.set_sort_func(sort_devices); + devices_box.set_filter_func(filter_paired); devices_box.get_style_context().add_class("bluetooth-devices-listbox"); devices_box.row_activated.connect((row) => { @@ -97,7 +86,12 @@ public class BluetoothIndicator : Bin { }); scrolled_window.add(devices_box); - main_page.pack_start(scrolled_window); + + // Footer + var footer = new Box(HORIZONTAL, 0); + pairing_button = new Button.with_label(_("Pairing")); + pairing_button.clicked.connect(on_pairing_clicked); + footer.pack_start(pairing_button); // Create our Bluetooth client client = new BluetoothClient(); @@ -115,11 +109,14 @@ public class BluetoothIndicator : Bin { client.global_state_changed.connect(on_client_state_changed); - // Pack and show add(ebox); - popover.add(stack); - stack.show_all(); - stack.set_visible_child_name("main"); + box.pack_start(header); + box.pack_start(new Separator(HORIZONTAL), false, false, 1); + box.pack_start(scrolled_window); + box.pack_start(new Separator(HORIZONTAL), false, false, 1); + box.pack_end(footer); + box.show_all(); + popover.add(box); show_all(); } @@ -158,17 +155,69 @@ public class BluetoothIndicator : Bin { }); } + private void on_pairing_clicked() { + // Get the first powered adapter + Adapter1 adapter = null; + client.get_adapters().foreach((a) => { + if (a.powered) { + adapter = a; + } + }); + + if (adapter == null) return; + + if (!pairing) { + // Start Bluetooth discovery if we're on the main page + adapter.start_discovery.begin((obj, res) => { + try { + adapter.start_discovery.end(res); + devices_box.set_filter_func(filter_unpaired); + pairing_button.label = _("Stop Pairing"); + pairing = true; + } catch (Error e) { + warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); + } + }); + } else { + // Stop Bluetooth discovery if we're on the pairing page + adapter.stop_discovery.begin((obj, res) => { + try { + adapter.stop_discovery.end(res); + devices_box.set_filter_func(filter_paired); + pairing_button.label = _("Pairing"); + pairing = false; + } catch (Error e) { + warning("Error stopping discovery on adapter %s: %s", adapter.alias, e.message); + } + }); + } + + devices_box.invalidate_filter(); + devices_box.invalidate_sort(); + } + private void add_device(Device1 device) { debug("Bluetooth device added: %s", device.alias); - var widget = new BluetoothDeviceWidget(device); + // Get the adapter that this device is paired with + Adapter1? adapter = null; + client.get_adapters().foreach((a) => { + if (((DBusProxy) a).get_object_path() == device.adapter) { + adapter = a; + return; // Exit the lambda + } + }); + + var widget = new BluetoothDeviceWidget(device, adapter); widget.properties_updated.connect(() => { client.check_powered(); + devices_box.invalidate_filter(); devices_box.invalidate_sort(); }); devices_box.add(widget); + devices_box.invalidate_filter(); devices_box.invalidate_sort(); } @@ -182,6 +231,7 @@ public class BluetoothIndicator : Bin { } }); + devices_box.invalidate_filter(); devices_box.invalidate_sort(); } @@ -199,6 +249,18 @@ public class BluetoothIndicator : Bin { else if (b_device.device.connected) return 1; // B should go before A else return strcmp(a_device.device.alias, b_device.device.alias); } + + private bool filter_paired(ListBoxRow row) { + var widget = row.get_child() as BluetoothDeviceWidget; + + return widget.device.paired; + } + + private bool filter_unpaired(ListBoxRow row) { + var widget = row.get_child() as BluetoothDeviceWidget; + + return !widget.device.paired; + } } public class BluetoothDeviceWidget : Box { @@ -207,7 +269,9 @@ public class BluetoothDeviceWidget : Box { private Label? status_label = null; private Revealer? revealer = null; private Button? connection_button = null; + private Button? forget_button = null; + public Adapter1 adapter { get; construct; } public Device1 device { get; construct; } public signal void properties_updated(); @@ -250,7 +314,12 @@ public class BluetoothDeviceWidget : Box { connection_button = new Button.with_label(""); connection_button.clicked.connect(on_connection_button_clicked); + forget_button = new Button.with_label(_("Forget Device")); + forget_button.get_style_context().add_class(STYLE_CLASS_DESTRUCTIVE_ACTION); + forget_button.clicked.connect(on_forget_clicked); + revealer_body.pack_start(connection_button); + revealer_body.pack_end(forget_button); revealer.add(revealer_body); // Signals @@ -268,9 +337,10 @@ public class BluetoothDeviceWidget : Box { show_all(); } - public BluetoothDeviceWidget(Device1 device) { + public BluetoothDeviceWidget(Device1 device, Adapter1 adapter) { Object( device: device, + adapter: adapter, orientation: Orientation.VERTICAL, spacing: 0 ); @@ -283,7 +353,7 @@ public class BluetoothDeviceWidget : Box { private void on_connection_button_clicked() { connection_button.sensitive = false; - if (device.connected) { + if (device.connected) { // Device is connected; disconnect it device.disconnect.begin((obj, res) => { try { device.disconnect.end(res); @@ -293,7 +363,7 @@ public class BluetoothDeviceWidget : Box { connection_button.sensitive = true; }); - } else { + } else if (!device.connected) { // Device isn't connected; connect it device.connect.begin((obj, res) => { try { device.connect.end(res); @@ -301,11 +371,66 @@ public class BluetoothDeviceWidget : Box { warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); } + connection_button.sensitive = true; + }); + } else if (!device.paired) { // Device isn't paired; pair it + device.pair.begin((obj, res) => { + try { + device.pair.end(res); + } catch (Error e) { + warning("Error pairing Bluetooth device %s: %s", device.alias, e.message); + } + connection_button.sensitive = true; }); } } + /** + * Handles when the forget device button is clicked. + * + * A dialog box will be shown to confirm that the user wishes to forget + * the device, meaning it will be unpaired from the adapter and have to + * be re-paired before being able to use it again. + */ + private void on_forget_clicked() { + var dialog = new MessageDialog( + null, + DialogFlags.MODAL, + MessageType.QUESTION, + ButtonsType.OK_CANCEL, + _("Are you sure you want to forget this device? You will have to pair it to use it again.") + ); + + // Register a handler for the response to the dialog + dialog.response.connect((response) => { + switch (response) { + case ResponseType.OK: // User confirmed removal of the device + // Get the path to this device + var path_str = ((DBusProxy) device).get_object_path(); + var path = new ObjectPath(path_str); + // Remove the device from the adapter it's connected to + adapter.remove_device.begin(path, (obj, res) => { + try { + adapter.remove_device.end(res); + } catch (Error e) { + warning("Error forgetting device %s: %s", device.alias, e.message); + } + }); + break; + default: // Any other response; do nothing + debug("Bluetooth forget dialog had result other than OK"); + break; + } + + // Destroy the dialog after a response has been received + dialog.destroy(); + }); + + // Show the dialog + dialog.show(); + } + private void update_status() { if (device.connected) { status_label.set_text(_("Connected")); @@ -318,11 +443,12 @@ public class BluetoothDeviceWidget : Box { // Device isn't paired if (!device.paired) { status_label.set_text(_("Not paired")); + connection_button.label = _("Pair Devices"); + forget_button.hide(); + } else { + forget_button.show(); } - // Only make the (dis)connect button clickable if the device is paired - connection_button.sensitive = device.paired; - properties_updated(); } } From 669b4b81cc5c2d88083858e18abb755fd107eb4a Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 22 Jan 2023 10:30:18 -0500 Subject: [PATCH 15/81] Set a filter for Bluetooth discovery so we only get actual discoverable devices Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index f45b20788..4f873cdd0 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -167,22 +167,37 @@ public class BluetoothIndicator : Bin { if (adapter == null) return; if (!pairing) { - // Start Bluetooth discovery if we're on the main page - adapter.start_discovery.begin((obj, res) => { + // Set the discovery filter + var properties = new HashTable(str_hash, str_equal); + properties["Discoverable"] = new Variant.boolean(true); + adapter.set_discovery_filter.begin(properties, (obj, res) => { try { - adapter.start_discovery.end(res); - devices_box.set_filter_func(filter_unpaired); - pairing_button.label = _("Stop Pairing"); - pairing = true; + adapter.set_discovery_filter.end(res); + + // Start Bluetooth discovery + adapter.start_discovery.begin((obj, res) => { + try { + adapter.start_discovery.end(res); + + // Set the pairing filter and update our state + devices_box.set_filter_func(filter_unpaired); + pairing_button.label = _("Stop Pairing"); + pairing = true; + } catch (Error e) { + warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); + } + }); } catch (Error e) { - warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); + warning("Error setting discovery filter on %s: %s", adapter.alias, e.message); } }); } else { - // Stop Bluetooth discovery if we're on the pairing page + // Stop Bluetooth discovery adapter.stop_discovery.begin((obj, res) => { try { adapter.stop_discovery.end(res); + + // Set the normal filter and update our state devices_box.set_filter_func(filter_paired); pairing_button.label = _("Pairing"); pairing = false; From f2d74e66f76080df41fa15a10cc49ebc559241b4 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 22 Jan 2023 10:35:17 -0500 Subject: [PATCH 16/81] Start/stop discovery on all adapters instead of the first one found Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 91 +++++++++---------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 4f873cdd0..6cd5a7691 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -156,57 +156,52 @@ public class BluetoothIndicator : Bin { } private void on_pairing_clicked() { - // Get the first powered adapter - Adapter1 adapter = null; - client.get_adapters().foreach((a) => { - if (a.powered) { - adapter = a; + // Iterate over all of the adapters, ignoring unpowered ones + client.get_adapters().foreach((adapter) => { + if (!adapter.powered) return; + + if (!pairing) { + // Set the discovery filter + var properties = new HashTable(str_hash, str_equal); + properties["Discoverable"] = new Variant.boolean(true); + adapter.set_discovery_filter.begin(properties, (obj, res) => { + try { + adapter.set_discovery_filter.end(res); + + // Start Bluetooth discovery + adapter.start_discovery.begin((obj, res) => { + try { + adapter.start_discovery.end(res); + + // Set the pairing filter and update our state + devices_box.set_filter_func(filter_unpaired); + pairing_button.label = _("Stop Pairing"); + pairing = true; + } catch (Error e) { + warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); + } + }); + } catch (Error e) { + warning("Error setting discovery filter on %s: %s", adapter.alias, e.message); + } + }); + } else { + // Stop Bluetooth discovery + adapter.stop_discovery.begin((obj, res) => { + try { + adapter.stop_discovery.end(res); + + // Set the normal filter and update our state + devices_box.set_filter_func(filter_paired); + pairing_button.label = _("Pairing"); + pairing = false; + } catch (Error e) { + warning("Error stopping discovery on adapter %s: %s", adapter.alias, e.message); + } + }); } }); - if (adapter == null) return; - - if (!pairing) { - // Set the discovery filter - var properties = new HashTable(str_hash, str_equal); - properties["Discoverable"] = new Variant.boolean(true); - adapter.set_discovery_filter.begin(properties, (obj, res) => { - try { - adapter.set_discovery_filter.end(res); - - // Start Bluetooth discovery - adapter.start_discovery.begin((obj, res) => { - try { - adapter.start_discovery.end(res); - - // Set the pairing filter and update our state - devices_box.set_filter_func(filter_unpaired); - pairing_button.label = _("Stop Pairing"); - pairing = true; - } catch (Error e) { - warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); - } - }); - } catch (Error e) { - warning("Error setting discovery filter on %s: %s", adapter.alias, e.message); - } - }); - } else { - // Stop Bluetooth discovery - adapter.stop_discovery.begin((obj, res) => { - try { - adapter.stop_discovery.end(res); - - // Set the normal filter and update our state - devices_box.set_filter_func(filter_paired); - pairing_button.label = _("Pairing"); - pairing = false; - } catch (Error e) { - warning("Error stopping discovery on adapter %s: %s", adapter.alias, e.message); - } - }); - } - devices_box.invalidate_filter(); devices_box.invalidate_sort(); } From fb3da60e8cbd06329ab19c2182d4f081b26d55cf Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 22 Jan 2023 10:48:16 -0500 Subject: [PATCH 17/81] Ensure that the correct height is given to the listbox Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 6cd5a7691..0882eca6e 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -71,7 +71,8 @@ public class BluetoothIndicator : Bin { var scrolled_window = new ScrolledWindow(null, null) { hscrollbar_policy = NEVER, min_content_height = 250, - max_content_height = 250 + max_content_height = 250, + propagate_natural_height = true }; devices_box = new ListBox() { selection_mode = NONE From f71bacef3ee7302d196744f3abf1a26a58b055f6 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 22 Jan 2023 10:56:45 -0500 Subject: [PATCH 18/81] Remove the Meson option for Bluetooth The reason for the option's addition no longer applies; the new Bluetooth indicator uses Bluez directly instead of gnome-bluetooth. Signed-off-by: Evan Maddock --- meson.build | 5 ----- meson_options.txt | 8 ++++++++ src/panel/applets/status/meson.build | 5 ----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/meson.build b/meson.build index 50879a0a2..3a6dae047 100644 --- a/meson.build +++ b/meson.build @@ -132,11 +132,6 @@ if prefix == '/usr' or prefix == '/usr/local' cdata.set_quoted('RAVEN_PLUGIN_DATADIR_SECONDARY', join_paths(secondary_datadir_root, 'raven-plugins')) endif -with_bluetooth = get_option('with-bluetooth') -if with_bluetooth == true - add_project_arguments('-D', 'with_bluetooth', language: 'vala') -endif - with_hibernate = get_option('with-hibernate') if with_hibernate == true add_project_arguments('-D', 'WITH_HIBERNATE', language: 'vala') diff --git a/meson_options.txt b/meson_options.txt index a1c06ac76..1206b04fd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,7 +1,15 @@ +<<<<<<< HEAD option('use-old-zenity', type: 'boolean', value: false, description: 'Use old zenity CLI API for out-of-process dialog handling') option('with-bluetooth', type: 'boolean', value: true, description: 'Enable Bluetooth (Vala option)') option('with-gnome-screensaver', type: 'boolean', value: false, description: 'Build using gnome-screensaver as a dependency') option('with-gtk-doc', type: 'boolean', value: true, description: 'Build gtk-doc documentation') +||||||| parent of b5dbd3bf (Remove the Meson option for Bluetooth) +option('with-stateless', type: 'boolean', value: false, description: 'Enable stateless XDG paths') +option('with-bluetooth', type: 'boolean', value: true, description: 'Enable Bluetooth (Vala option)') +======= +option('with-stateless', type: 'boolean', value: false, description: 'Enable stateless XDG paths') +option('with-bluetooth', type: 'boolean', value: true, description: 'Enable Bluetooth (Vala option)', deprecated: true) +>>>>>>> b5dbd3bf (Remove the Meson option for Bluetooth) option('with-hibernate', type: 'boolean', value: true, description: 'Include support for system hibernation') option('with-libuuid-time-safe', type: 'boolean', value: true, description: 'Enable use of LIBUUID.generate_time_safe (Vala option)') option('with-polkit', type: 'boolean', value: true, description: 'Enable PolKit support') diff --git a/src/panel/applets/status/meson.build b/src/panel/applets/status/meson.build index a1aa44cc4..7eacf8cb0 100644 --- a/src/panel/applets/status/meson.build +++ b/src/panel/applets/status/meson.build @@ -41,11 +41,6 @@ applet_status_deps = [ meson.get_compiler('c').find_library('m', required: false), ] -# TODO: Nuke -if with_bluetooth == true - applet_status_deps += dependency('gnome-bluetooth-1.0', version: '>= 3.34.0') -endif - shared_library( 'statusapplet', sources: applet_status_sources, From b3322ea1aa90e763e9a658c338a826769afe7c34 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 22 Jan 2023 16:46:40 -0500 Subject: [PATCH 19/81] Use better wording for pairing button Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 0882eca6e..70a7f27c1 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -90,7 +90,7 @@ public class BluetoothIndicator : Bin { // Footer var footer = new Box(HORIZONTAL, 0); - pairing_button = new Button.with_label(_("Pairing")); + pairing_button = new Button.with_label(_("Start Pairing")); pairing_button.clicked.connect(on_pairing_clicked); footer.pack_start(pairing_button); @@ -194,7 +194,7 @@ public class BluetoothIndicator : Bin { // Set the normal filter and update our state devices_box.set_filter_func(filter_paired); - pairing_button.label = _("Pairing"); + pairing_button.label = _("Start Pairing"); pairing = false; } catch (Error e) { warning("Error stopping discovery on adapter %s: %s", adapter.alias, e.message); From 9427cfb0a90f187da51312fa30e784897902263b Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 24 Jan 2023 10:28:56 -0500 Subject: [PATCH 20/81] Reset revealer state when DBus operations are successful Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 70a7f27c1..14ddbaeb2 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -178,6 +178,7 @@ public class BluetoothIndicator : Bin { devices_box.set_filter_func(filter_unpaired); pairing_button.label = _("Stop Pairing"); pairing = true; + reset_revealers(); } catch (Error e) { warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); } @@ -196,6 +197,7 @@ public class BluetoothIndicator : Bin { devices_box.set_filter_func(filter_paired); pairing_button.label = _("Start Pairing"); pairing = false; + reset_revealers(); } catch (Error e) { warning("Error stopping discovery on adapter %s: %s", adapter.alias, e.message); } @@ -272,6 +274,19 @@ public class BluetoothIndicator : Bin { return !widget.device.paired; } + + /** + * Iterate over all devices in the list box and closes any open + * revealers. + */ + private void reset_revealers() { + devices_box.foreach((row) => { + var widget = ((ListBoxRow) row).get_child() as BluetoothDeviceWidget; + if (widget.revealer_showing()) { + widget.toggle_revealer(); + } + }); + } } public class BluetoothDeviceWidget : Box { @@ -357,6 +372,10 @@ public class BluetoothDeviceWidget : Box { ); } + public bool revealer_showing() { + return revealer.reveal_child; + } + public void toggle_revealer() { revealer.reveal_child = !revealer.reveal_child; } @@ -368,6 +387,8 @@ public class BluetoothDeviceWidget : Box { device.disconnect.begin((obj, res) => { try { device.disconnect.end(res); + + toggle_revealer(); } catch (Error e) { warning("Failed to disconnect Bluetooth device %s: %s", device.alias, e.message); } @@ -378,6 +399,8 @@ public class BluetoothDeviceWidget : Box { device.connect.begin((obj, res) => { try { device.connect.end(res); + + toggle_revealer(); } catch (Error e) { warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); } @@ -388,6 +411,8 @@ public class BluetoothDeviceWidget : Box { device.pair.begin((obj, res) => { try { device.pair.end(res); + + toggle_revealer(); } catch (Error e) { warning("Error pairing Bluetooth device %s: %s", device.alias, e.message); } From e314ff1e7a5ba97bb36e0f7439b81dac8da9e9ee Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 24 Jan 2023 19:39:13 -0500 Subject: [PATCH 21/81] Always set adapters to be discovering While not ideal, I suspect this is the only way to reliably pair devices. Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 22 +++++ .../applets/status/BluetoothIndicator.vala | 94 ++----------------- 2 files changed, 28 insertions(+), 88 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 93d8f232c..60f693f1f 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -166,6 +166,28 @@ class BluetoothClient : GLib.Object { if (powered == null) return; set_last_powered.begin(); }); + + if (!adapter.powered) return; + + // Set the discovery filter + var properties = new HashTable(str_hash, str_equal); + properties["Discoverable"] = new Variant.boolean(true); + adapter.set_discovery_filter.begin(properties, (obj, res) => { + try { + adapter.set_discovery_filter.end(res); + + // Start Bluetooth discovery + adapter.start_discovery.begin((obj, res) => { + try { + adapter.start_discovery.end(res); + } catch (Error e) { + warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); + } + }); + } catch (Error e) { + warning("Error setting discovery filter on %s: %s", adapter.alias, e.message); + } + }); } else if (iface is Device1) { unowned Device1 device = iface as Device1; device_added(device); diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 14ddbaeb2..c688be6d8 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -22,12 +22,9 @@ public class BluetoothIndicator : Bin { private ListBox? devices_box = null; private Switch? bluetooth_switch = null; - private Button? pairing_button = null; private BluetoothClient client; - public bool pairing { get; private set; default = false; } - construct { get_style_context().add_class("bluetooth-applet-popover"); @@ -78,7 +75,6 @@ public class BluetoothIndicator : Bin { selection_mode = NONE }; devices_box.set_sort_func(sort_devices); - devices_box.set_filter_func(filter_paired); devices_box.get_style_context().add_class("bluetooth-devices-listbox"); devices_box.row_activated.connect((row) => { @@ -88,12 +84,6 @@ public class BluetoothIndicator : Bin { scrolled_window.add(devices_box); - // Footer - var footer = new Box(HORIZONTAL, 0); - pairing_button = new Button.with_label(_("Start Pairing")); - pairing_button.clicked.connect(on_pairing_clicked); - footer.pack_start(pairing_button); - // Create our Bluetooth client client = new BluetoothClient(); @@ -115,7 +105,6 @@ public class BluetoothIndicator : Bin { box.pack_start(new Separator(HORIZONTAL), false, false, 1); box.pack_start(scrolled_window); box.pack_start(new Separator(HORIZONTAL), false, false, 1); - box.pack_end(footer); box.show_all(); popover.add(box); show_all(); @@ -156,59 +145,6 @@ public class BluetoothIndicator : Bin { }); } - private void on_pairing_clicked() { - // Iterate over all of the adapters, ignoring unpowered ones - client.get_adapters().foreach((adapter) => { - if (!adapter.powered) return; - - if (!pairing) { - // Set the discovery filter - var properties = new HashTable(str_hash, str_equal); - properties["Discoverable"] = new Variant.boolean(true); - adapter.set_discovery_filter.begin(properties, (obj, res) => { - try { - adapter.set_discovery_filter.end(res); - - // Start Bluetooth discovery - adapter.start_discovery.begin((obj, res) => { - try { - adapter.start_discovery.end(res); - - // Set the pairing filter and update our state - devices_box.set_filter_func(filter_unpaired); - pairing_button.label = _("Stop Pairing"); - pairing = true; - reset_revealers(); - } catch (Error e) { - warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); - } - }); - } catch (Error e) { - warning("Error setting discovery filter on %s: %s", adapter.alias, e.message); - } - }); - } else { - // Stop Bluetooth discovery - adapter.stop_discovery.begin((obj, res) => { - try { - adapter.stop_discovery.end(res); - - // Set the normal filter and update our state - devices_box.set_filter_func(filter_paired); - pairing_button.label = _("Start Pairing"); - pairing = false; - reset_revealers(); - } catch (Error e) { - warning("Error stopping discovery on adapter %s: %s", adapter.alias, e.message); - } - }); - } - }); - - devices_box.invalidate_filter(); - devices_box.invalidate_sort(); - } - private void add_device(Device1 device) { debug("Bluetooth device added: %s", device.alias); @@ -225,12 +161,10 @@ public class BluetoothIndicator : Bin { widget.properties_updated.connect(() => { client.check_powered(); - devices_box.invalidate_filter(); devices_box.invalidate_sort(); }); devices_box.add(widget); - devices_box.invalidate_filter(); devices_box.invalidate_sort(); } @@ -244,7 +178,6 @@ public class BluetoothIndicator : Bin { } }); - devices_box.invalidate_filter(); devices_box.invalidate_sort(); } @@ -263,18 +196,6 @@ public class BluetoothIndicator : Bin { else return strcmp(a_device.device.alias, b_device.device.alias); } - private bool filter_paired(ListBoxRow row) { - var widget = row.get_child() as BluetoothDeviceWidget; - - return widget.device.paired; - } - - private bool filter_unpaired(ListBoxRow row) { - var widget = row.get_child() as BluetoothDeviceWidget; - - return !widget.device.paired; - } - /** * Iterate over all devices in the list box and closes any open * revealers. @@ -468,20 +389,17 @@ public class BluetoothDeviceWidget : Box { } private void update_status() { - if (device.connected) { - status_label.set_text(_("Connected")); - connection_button.label = _("Disconnect"); - } else { - status_label.set_text(_("Disconnected")); - connection_button.label = _("Connect"); - } - - // Device isn't paired if (!device.paired) { status_label.set_text(_("Not paired")); connection_button.label = _("Pair Devices"); forget_button.hide(); + } else if (device.connected) { + status_label.set_text(_("Connected")); + connection_button.label = _("Disconnect"); + forget_button.show(); } else { + status_label.set_text(_("Disconnected")); + connection_button.label = _("Connect"); forget_button.show(); } From ab122319cfcebbe7692bd636f898853113c0492b Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Feb 2023 11:13:34 -0500 Subject: [PATCH 22/81] Forget about Bluetooth pairing; leave it to the Settings Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 22 ------------------- src/panel/applets/status/BluetoothDBus.vala | 10 --------- .../applets/status/BluetoothIndicator.vala | 20 +---------------- 3 files changed, 1 insertion(+), 51 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 60f693f1f..93d8f232c 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -166,28 +166,6 @@ class BluetoothClient : GLib.Object { if (powered == null) return; set_last_powered.begin(); }); - - if (!adapter.powered) return; - - // Set the discovery filter - var properties = new HashTable(str_hash, str_equal); - properties["Discoverable"] = new Variant.boolean(true); - adapter.set_discovery_filter.begin(properties, (obj, res) => { - try { - adapter.set_discovery_filter.end(res); - - // Start Bluetooth discovery - adapter.start_discovery.begin((obj, res) => { - try { - adapter.start_discovery.end(res); - } catch (Error e) { - warning("Error beginning discovery on adapter %s: %s", adapter.alias, e.message); - } - }); - } catch (Error e) { - warning("Error setting discovery filter on %s: %s", adapter.alias, e.message); - } - }); } else if (iface is Device1) { unowned Device1 device = iface as Device1; device_added(device); diff --git a/src/panel/applets/status/BluetoothDBus.vala b/src/panel/applets/status/BluetoothDBus.vala index 8ce738c25..f1a09578c 100644 --- a/src/panel/applets/status/BluetoothDBus.vala +++ b/src/panel/applets/status/BluetoothDBus.vala @@ -63,13 +63,3 @@ public interface Device1 : GLib.Object { public async abstract void pair() throws GLib.DBusError, GLib.IOError; public async abstract void cancel_pairing() throws GLib.DBusError, GLib.IOError; } - -/** - * Definition of the Bluez AgentManager1 interface. - */ -[DBus (name = "org.bluez.AgentManager1")] -public interface AgentManager1 : GLib.Object { - public async abstract void register_agent(GLib.ObjectPath agent, string capability) throws GLib.DBusError, GLib.IOError; - public async abstract void unregister_agent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; - public async abstract void request_default_agent(GLib.ObjectPath agent) throws GLib.DBusError, GLib.IOError; -} diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index c688be6d8..f76f94475 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -326,18 +326,6 @@ public class BluetoothDeviceWidget : Box { warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); } - connection_button.sensitive = true; - }); - } else if (!device.paired) { // Device isn't paired; pair it - device.pair.begin((obj, res) => { - try { - device.pair.end(res); - - toggle_revealer(); - } catch (Error e) { - warning("Error pairing Bluetooth device %s: %s", device.alias, e.message); - } - connection_button.sensitive = true; }); } @@ -389,18 +377,12 @@ public class BluetoothDeviceWidget : Box { } private void update_status() { - if (!device.paired) { - status_label.set_text(_("Not paired")); - connection_button.label = _("Pair Devices"); - forget_button.hide(); - } else if (device.connected) { + if (device.connected) { status_label.set_text(_("Connected")); connection_button.label = _("Disconnect"); - forget_button.show(); } else { status_label.set_text(_("Disconnected")); connection_button.label = _("Connect"); - forget_button.show(); } properties_updated(); From 45016738e6b78fb6a0c7331858d1279bf88f7596 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Feb 2023 11:28:27 -0500 Subject: [PATCH 23/81] Remove forget device button because dialogs from the panel are Bad TM Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index f76f94475..90bb6847b 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -216,7 +216,6 @@ public class BluetoothDeviceWidget : Box { private Label? status_label = null; private Revealer? revealer = null; private Button? connection_button = null; - private Button? forget_button = null; public Adapter1 adapter { get; construct; } public Device1 device { get; construct; } @@ -261,12 +260,7 @@ public class BluetoothDeviceWidget : Box { connection_button = new Button.with_label(""); connection_button.clicked.connect(on_connection_button_clicked); - forget_button = new Button.with_label(_("Forget Device")); - forget_button.get_style_context().add_class(STYLE_CLASS_DESTRUCTIVE_ACTION); - forget_button.clicked.connect(on_forget_clicked); - revealer_body.pack_start(connection_button); - revealer_body.pack_end(forget_button); revealer.add(revealer_body); // Signals @@ -331,51 +325,6 @@ public class BluetoothDeviceWidget : Box { } } - /** - * Handles when the forget device button is clicked. - * - * A dialog box will be shown to confirm that the user wishes to forget - * the device, meaning it will be unpaired from the adapter and have to - * be re-paired before being able to use it again. - */ - private void on_forget_clicked() { - var dialog = new MessageDialog( - null, - DialogFlags.MODAL, - MessageType.QUESTION, - ButtonsType.OK_CANCEL, - _("Are you sure you want to forget this device? You will have to pair it to use it again.") - ); - - // Register a handler for the response to the dialog - dialog.response.connect((response) => { - switch (response) { - case ResponseType.OK: // User confirmed removal of the device - // Get the path to this device - var path_str = ((DBusProxy) device).get_object_path(); - var path = new ObjectPath(path_str); - // Remove the device from the adapter it's connected to - adapter.remove_device.begin(path, (obj, res) => { - try { - adapter.remove_device.end(res); - } catch (Error e) { - warning("Error forgetting device %s: %s", device.alias, e.message); - } - }); - break; - default: // Any other response; do nothing - debug("Bluetooth forget dialog had result other than OK"); - break; - } - - // Destroy the dialog after a response has been received - dialog.destroy(); - }); - - // Show the dialog - dialog.show(); - } - private void update_status() { if (device.connected) { status_label.set_text(_("Connected")); From f5fcd919292f2774a3878857732c63e70fe5e339 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Feb 2023 11:37:28 -0500 Subject: [PATCH 24/81] Make Bluetooth row widget subclass ListBoxRow Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 90bb6847b..6018190bd 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -78,7 +78,7 @@ public class BluetoothIndicator : Bin { devices_box.get_style_context().add_class("bluetooth-devices-listbox"); devices_box.row_activated.connect((row) => { - var widget = row.get_child() as BluetoothDeviceWidget; + var widget = row as BluetoothDeviceWidget; widget.toggle_revealer(); }); @@ -148,16 +148,7 @@ public class BluetoothIndicator : Bin { private void add_device(Device1 device) { debug("Bluetooth device added: %s", device.alias); - // Get the adapter that this device is paired with - Adapter1? adapter = null; - client.get_adapters().foreach((a) => { - if (((DBusProxy) a).get_object_path() == device.adapter) { - adapter = a; - return; // Exit the lambda - } - }); - - var widget = new BluetoothDeviceWidget(device, adapter); + var widget = new BluetoothDeviceWidget(device); widget.properties_updated.connect(() => { client.check_powered(); @@ -172,7 +163,7 @@ public class BluetoothIndicator : Bin { debug("Bluetooth device removed: %s", device.alias); devices_box.foreach((row) => { - var child = ((ListBoxRow) row).get_child() as BluetoothDeviceWidget; + var child = row as BluetoothDeviceWidget; if (child.device.address == device.address) { row.destroy(); } @@ -187,8 +178,8 @@ public class BluetoothIndicator : Bin { * Items are sorted alphabetically, with connected devices at the top of the list. */ private int sort_devices(ListBoxRow a, ListBoxRow b) { - var a_device = a.get_child() as BluetoothDeviceWidget; - var b_device = b.get_child() as BluetoothDeviceWidget; + var a_device = a as BluetoothDeviceWidget; + var b_device = b as BluetoothDeviceWidget; if (a_device.device.connected && b_device.device.connected) return strcmp(a_device.device.alias, b_device.device.alias); else if (a_device.device.connected) return -1; // A should go before B @@ -202,7 +193,7 @@ public class BluetoothIndicator : Bin { */ private void reset_revealers() { devices_box.foreach((row) => { - var widget = ((ListBoxRow) row).get_child() as BluetoothDeviceWidget; + var widget = row as BluetoothDeviceWidget; if (widget.revealer_showing()) { widget.toggle_revealer(); } @@ -210,14 +201,13 @@ public class BluetoothIndicator : Bin { } } -public class BluetoothDeviceWidget : Box { +public class BluetoothDeviceWidget : ListBoxRow { private Image? image = null; private Label? name_label = null; private Label? status_label = null; private Revealer? revealer = null; private Button? connection_button = null; - public Adapter1 adapter { get; construct; } public Device1 device { get; construct; } public signal void properties_updated(); @@ -226,6 +216,7 @@ public class BluetoothDeviceWidget : Box { get_style_context().add_class("bluetooth-widget"); // Body + var box = new Box(Orientation.VERTICAL, 0); var grid = new Grid(); image = new Image.from_icon_name(device.icon ?? "bluetooth", LARGE_TOOLBAR) { @@ -271,20 +262,16 @@ public class BluetoothDeviceWidget : Box { grid.attach(name_label, 1, 0); grid.attach(status_label, 1, 1); - pack_start(grid); - pack_start(revealer); + box.pack_start(grid); + box.pack_start(revealer); + add(box); update_status(); show_all(); } - public BluetoothDeviceWidget(Device1 device, Adapter1 adapter) { - Object( - device: device, - adapter: adapter, - orientation: Orientation.VERTICAL, - spacing: 0 - ); + public BluetoothDeviceWidget(Device1 device) { + Object(device: device); } public bool revealer_showing() { From 7f107a8926650cada918dfedfa49a3acdfcc48e2 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Feb 2023 11:43:08 -0500 Subject: [PATCH 25/81] Add slightly more spacing around separators Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 6018190bd..cfc1aa045 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -102,9 +102,9 @@ public class BluetoothIndicator : Bin { add(ebox); box.pack_start(header); - box.pack_start(new Separator(HORIZONTAL), false, false, 1); + box.pack_start(new Separator(HORIZONTAL), true, true, 2); box.pack_start(scrolled_window); - box.pack_start(new Separator(HORIZONTAL), false, false, 1); + box.pack_start(new Separator(HORIZONTAL), true, true, 2); box.show_all(); popover.add(box); show_all(); From b3a07047c7bedc2dc5432490852f2daca46806ea Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Feb 2023 13:15:54 -0500 Subject: [PATCH 26/81] Minor style enhancements Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index cfc1aa045..dce7f7a62 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -37,15 +37,21 @@ public class BluetoothIndicator : Bin { // Create our popover popover = new Budgie.Popover(ebox); + popover.set_size_request(200, -1); + popover.hide.connect(() => { + reset_revealers(); + }); var box = new Box(VERTICAL, 0); // Header var header = new Box(HORIZONTAL, 0); - header.get_style_context().add_class("bluetooth-applet-header"); + header.get_style_context().add_class("bluetooth-popover-header"); // Header label - var switch_label = new Label(_("Bluetooth")); - switch_label.get_style_context().add_class("dim-label"); + var switch_label = new Label(_("Bluetooth")) { + halign = START, + }; + switch_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); // Settings button var button = new Button.from_icon_name("preferences-system-symbolic", MENU) { @@ -61,8 +67,8 @@ public class BluetoothIndicator : Bin { bluetooth_switch.notify["active"].connect(on_switch_activate); header.pack_start(switch_label); - header.pack_end(bluetooth_switch); - header.pack_end(button, false, false, 0); + header.pack_end(bluetooth_switch, false, false); + header.pack_end(button, false, false); // Devices var scrolled_window = new ScrolledWindow(null, null) { @@ -75,7 +81,7 @@ public class BluetoothIndicator : Bin { selection_mode = NONE }; devices_box.set_sort_func(sort_devices); - devices_box.get_style_context().add_class("bluetooth-devices-listbox"); + devices_box.get_style_context().add_class("bluetooth-device-listbox"); devices_box.row_activated.connect((row) => { var widget = row as BluetoothDeviceWidget; @@ -213,17 +219,16 @@ public class BluetoothDeviceWidget : ListBoxRow { public signal void properties_updated(); construct { - get_style_context().add_class("bluetooth-widget"); + get_style_context().add_class("bluetooth-device-row"); // Body var box = new Box(Orientation.VERTICAL, 0); - var grid = new Grid(); - - image = new Image.from_icon_name(device.icon ?? "bluetooth", LARGE_TOOLBAR) { - halign = START, - margin_end = 6 + var grid = new Grid() { + column_spacing = 6, }; + image = new Image.from_icon_name(device.icon ?? "bluetooth", LARGE_TOOLBAR); + name_label = new Label(device.alias) { valign = CENTER, xalign = 0.0f, @@ -235,9 +240,8 @@ public class BluetoothDeviceWidget : ListBoxRow { status_label = new Label(null) { halign = START, - hexpand = true }; - status_label.get_style_context().add_class("dim-label"); + status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); // Revealer stuff revealer = new Revealer() { @@ -245,7 +249,7 @@ public class BluetoothDeviceWidget : ListBoxRow { transition_duration = 250, transition_type = RevealerTransitionType.SLIDE_DOWN }; - revealer.get_style_context().add_class("bluetooth-widget-revealer"); + revealer.get_style_context().add_class("bluetooth-device-row-revealer"); var revealer_body = new Box(HORIZONTAL, 0); connection_button = new Button.with_label(""); @@ -258,9 +262,9 @@ public class BluetoothDeviceWidget : ListBoxRow { ((DBusProxy) device).g_properties_changed.connect(update_status); // Packing - grid.attach(image, 0, 0); - grid.attach(name_label, 1, 0); - grid.attach(status_label, 1, 1); + grid.attach(image, 0, 0, 2, 2); + grid.attach(name_label, 2, 0, 2, 1); + grid.attach(status_label, 2, 1, 2, 1); box.pack_start(grid); box.pack_start(revealer); From df858fa13d2286ef9c5f701f8cb83eee20b59d8d Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Feb 2023 13:21:21 -0500 Subject: [PATCH 27/81] Filter out unpaired Bluetooth devices Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index dce7f7a62..232e386b3 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -80,6 +80,7 @@ public class BluetoothIndicator : Bin { devices_box = new ListBox() { selection_mode = NONE }; + devices_box.set_filter_func(filter_paired_devices); devices_box.set_sort_func(sort_devices); devices_box.get_style_context().add_class("bluetooth-device-listbox"); @@ -193,6 +194,13 @@ public class BluetoothIndicator : Bin { else return strcmp(a_device.device.alias, b_device.device.alias); } + /** + * Filters out any unpaired devices from our listbox. + */ + private bool filter_paired_devices(ListBoxRow row) { + return ((BluetoothDeviceWidget) row).device.paired; + } + /** * Iterate over all devices in the list box and closes any open * revealers. From fa2467222226ee1180e3eeb51cbb2aa55384dfdd Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Feb 2023 13:28:08 -0500 Subject: [PATCH 28/81] Rename row widget to be shorter and add consistent style classes Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 232e386b3..08aef9bc1 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -26,8 +26,6 @@ public class BluetoothIndicator : Bin { private BluetoothClient client; construct { - get_style_context().add_class("bluetooth-applet-popover"); - image = new Image.from_icon_name("bluetooth-active-symbolic", IconSize.MENU); ebox = new EventBox(); @@ -38,6 +36,7 @@ public class BluetoothIndicator : Bin { // Create our popover popover = new Budgie.Popover(ebox); popover.set_size_request(200, -1); + popover.get_style_context().add_class("bluetooth-applet-popover"); popover.hide.connect(() => { reset_revealers(); }); @@ -58,6 +57,7 @@ public class BluetoothIndicator : Bin { tooltip_text = _("Bluetooth Settings") }; button.get_style_context().add_class(STYLE_CLASS_FLAT); + button.get_style_context().remove_class(STYLE_CLASS_BUTTON); button.clicked.connect(on_settings_activate); // Bluetooth switch @@ -85,7 +85,7 @@ public class BluetoothIndicator : Bin { devices_box.get_style_context().add_class("bluetooth-device-listbox"); devices_box.row_activated.connect((row) => { - var widget = row as BluetoothDeviceWidget; + var widget = row as BTDeviceRow; widget.toggle_revealer(); }); @@ -155,7 +155,7 @@ public class BluetoothIndicator : Bin { private void add_device(Device1 device) { debug("Bluetooth device added: %s", device.alias); - var widget = new BluetoothDeviceWidget(device); + var widget = new BTDeviceRow(device); widget.properties_updated.connect(() => { client.check_powered(); @@ -170,7 +170,7 @@ public class BluetoothIndicator : Bin { debug("Bluetooth device removed: %s", device.alias); devices_box.foreach((row) => { - var child = row as BluetoothDeviceWidget; + var child = row as BTDeviceRow; if (child.device.address == device.address) { row.destroy(); } @@ -185,8 +185,8 @@ public class BluetoothIndicator : Bin { * Items are sorted alphabetically, with connected devices at the top of the list. */ private int sort_devices(ListBoxRow a, ListBoxRow b) { - var a_device = a as BluetoothDeviceWidget; - var b_device = b as BluetoothDeviceWidget; + var a_device = a as BTDeviceRow; + var b_device = b as BTDeviceRow; if (a_device.device.connected && b_device.device.connected) return strcmp(a_device.device.alias, b_device.device.alias); else if (a_device.device.connected) return -1; // A should go before B @@ -198,7 +198,7 @@ public class BluetoothIndicator : Bin { * Filters out any unpaired devices from our listbox. */ private bool filter_paired_devices(ListBoxRow row) { - return ((BluetoothDeviceWidget) row).device.paired; + return ((BTDeviceRow) row).device.paired; } /** @@ -207,7 +207,7 @@ public class BluetoothIndicator : Bin { */ private void reset_revealers() { devices_box.foreach((row) => { - var widget = row as BluetoothDeviceWidget; + var widget = row as BTDeviceRow; if (widget.revealer_showing()) { widget.toggle_revealer(); } @@ -215,7 +215,10 @@ public class BluetoothIndicator : Bin { } } -public class BluetoothDeviceWidget : ListBoxRow { +/** + * Widget for displaying a Bluetooth device in a ListBox. + */ +public class BTDeviceRow : ListBoxRow { private Image? image = null; private Label? name_label = null; private Label? status_label = null; @@ -236,6 +239,7 @@ public class BluetoothDeviceWidget : ListBoxRow { }; image = new Image.from_icon_name(device.icon ?? "bluetooth", LARGE_TOOLBAR); + image.get_style_context().add_class("bluetooth-device-image"); name_label = new Label(device.alias) { valign = CENTER, @@ -245,10 +249,12 @@ public class BluetoothDeviceWidget : ListBoxRow { hexpand = true, tooltip_text = device.alias }; + name_label.get_style_context().add_class("bluetooth-device-name"); status_label = new Label(null) { halign = START, }; + status_label.get_style_context().add_class("bluetooth-device-status"); status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); // Revealer stuff @@ -257,7 +263,7 @@ public class BluetoothDeviceWidget : ListBoxRow { transition_duration = 250, transition_type = RevealerTransitionType.SLIDE_DOWN }; - revealer.get_style_context().add_class("bluetooth-device-row-revealer"); + revealer.get_style_context().add_class("bluetooth-device-revealer"); var revealer_body = new Box(HORIZONTAL, 0); connection_button = new Button.with_label(""); @@ -282,7 +288,7 @@ public class BluetoothDeviceWidget : ListBoxRow { show_all(); } - public BluetoothDeviceWidget(Device1 device) { + public BTDeviceRow(Device1 device) { Object(device: device); } From c48641bd79b2eab23ccdf04c78ca9b41a3e8d44c Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 8 Feb 2023 10:15:41 -0500 Subject: [PATCH 29/81] Add an expander indicator to Bluetooth device rows Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 08aef9bc1..f308d92d5 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -222,6 +222,7 @@ public class BTDeviceRow : ListBoxRow { private Image? image = null; private Label? name_label = null; private Label? status_label = null; + private Image? expand_icon = null; private Revealer? revealer = null; private Button? connection_button = null; @@ -257,6 +258,8 @@ public class BTDeviceRow : ListBoxRow { status_label.get_style_context().add_class("bluetooth-device-status"); status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); + expand_icon = new Image.from_icon_name("pan-end-symbolic", BUTTON); + // Revealer stuff revealer = new Revealer() { reveal_child = false, @@ -279,6 +282,7 @@ public class BTDeviceRow : ListBoxRow { grid.attach(image, 0, 0, 2, 2); grid.attach(name_label, 2, 0, 2, 1); grid.attach(status_label, 2, 1, 2, 1); + grid.attach(expand_icon, 4, 0, 2, 2); box.pack_start(grid); box.pack_start(revealer); @@ -298,6 +302,11 @@ public class BTDeviceRow : ListBoxRow { public void toggle_revealer() { revealer.reveal_child = !revealer.reveal_child; + if (revealer.reveal_child) { + expand_icon.set_from_icon_name("pan-down-symbolic", BUTTON); + } else { + expand_icon.set_from_icon_name("pan-end-symbolic", BUTTON); + } } private void on_connection_button_clicked() { From 7d01de39a8e2625890fc5befcf00792ae2a76f26 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 8 Feb 2023 10:36:05 -0500 Subject: [PATCH 30/81] Show connected devices, paired or not Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index f308d92d5..60a03b035 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -198,7 +198,7 @@ public class BluetoothIndicator : Bin { * Filters out any unpaired devices from our listbox. */ private bool filter_paired_devices(ListBoxRow row) { - return ((BTDeviceRow) row).device.paired; + return ((BTDeviceRow) row).device.paired || ((BTDeviceRow) row).device.connected; } /** From c6dfedcfd028b1f10e4e358de30f1f676927f5b6 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Thu, 9 Feb 2023 14:33:59 -0500 Subject: [PATCH 31/81] Implement power display for Bluetooth devices Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 105 ++++------------- .../applets/status/BluetoothIndicator.vala | 108 +++++++++++++++++- 2 files changed, 130 insertions(+), 83 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 93d8f232c..8c2ec8719 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -19,6 +19,7 @@ const string BLUEZ_DBUS_NAME = "org.bluez"; const string BLUEZ_MANAGER_PATH = "/"; const string BLUEZ_ADAPTER_INTERFACE = "org.bluez.Adapter1"; const string BLUEZ_DEVICE_INTERFACE = "org.bluez.Device1"; +const string BLUETOOTH_ADDRESS_PREFIX = "/org/bluez/"; class BluetoothClient : GLib.Object { private Cancellable cancellable; @@ -26,10 +27,6 @@ class BluetoothClient : GLib.Object { private DBusObjectManagerClient object_manager; private Client upower_client; - private HashTable upower_devices; - - private bool bluez_devices_coldplugged = false; - public bool has_adapter { get; private set; default = false; } public bool is_connected { get; private set; default = false; } public bool is_enabled { get; private set; default = false; } @@ -40,12 +37,15 @@ class BluetoothClient : GLib.Object { public signal void device_added(Device1 device); /** Signal emitted when a Bluetooth device has been removed. */ public signal void device_removed(Device1 device); + /** Signal emitted when a UPower device for a Bluetooth device has been detected. */ + public signal void upower_device_added(Up.Device up_device); + /** Signal emitted when a UPower device for a Bluetooth device has been removed. */ + public signal void upower_device_removed(string object_path); /** Signal emitted when our powered or connected state changes. */ public signal void global_state_changed(bool enabled, bool connected); construct { cancellable = new Cancellable(); - upower_devices = new HashTable(str_hash, str_equal); // Set up our UPower client create_upower_client.begin(); @@ -87,10 +87,7 @@ class BluetoothClient : GLib.Object { upower_client.device_added.connect(upower_device_added_cb); upower_client.device_removed.connect(upower_device_removed_cb); - // Maybe coldplug UPower devices - if (bluez_devices_coldplugged) { - coldplug_client(); - } + coldplug_client(); } catch (Error e) { critical("Error creating UPower client: %s", e.message); } @@ -132,28 +129,6 @@ class BluetoothClient : GLib.Object { retrieve_finished = true; } - // private BluetoothDevice? get_device_with_address(string address) { - // var num_items = devices.get_n_items(); - - // for (var i = 0; i < num_items; i++) { - // var device = devices.get_item(i) as BluetoothDevice; - // if (device.address == address) return device; - // } - - // return null; - // } - - // private BluetoothDevice? get_device_with_object_path(string object_path) { - // var num_items = devices.get_n_items(); - - // for (var i = 0; i < num_items; i++) { - // var device = devices.get_item(i) as BluetoothDevice; - // if (device.get_object_path() == object_path) return device; - // } - - // return null; - // } - /** * Handles the addition of a DBus object interface. */ @@ -196,32 +171,13 @@ class BluetoothClient : GLib.Object { */ private void upower_device_added_cb(Device up_device) { var serial = up_device.serial; - message("upower_device_added_cb"); // Make sure the device has a valid Bluetooth address - if (serial == null || !is_valid_address(serial)) { - return; - } + if (serial == null || !is_valid_address(serial)) return; - // Get the device with the address - string? key = null; - Device? value = null; - var found = upower_devices.lookup_extended(up_device.get_object_path(), out key, out value); - if (found) message("Key in HashTable found: %s", key); - else message("Key not found. Sadge :("); - - // if (device == null) { - // warning("Could not find Bluetooth device for UPower device with serial '%s'", serial); - // return; - // } - - // // Connect signals - // up_device.notify["battery-level"].connect(() => device.update_battery(up_device)); - // up_device.notify["percentage"].connect(() => device.update_battery(up_device)); - - // // Update the power properties - // device.set_upower_device(up_device); - // device.update_battery(up_device); + if (!up_device.native_path.has_prefix(BLUETOOTH_ADDRESS_PREFIX)) return; + + upower_device_added(up_device); } /** @@ -231,19 +187,9 @@ class BluetoothClient : GLib.Object { * association removed, and its battery properties reset. */ private void upower_device_removed_cb(string object_path) { - // var device = get_device_with_object_path(object_path); - - // if (device == null) { - // return; - // } - - // debug("Removing Upower Device '%s' for Bluetooth device '%s'", object_path, device.get_object_path()); + if (!object_path.has_prefix(BLUETOOTH_ADDRESS_PREFIX)) return; - // // Reset device power properties - // device.set_upower_device(null); - // device.battery_type = BatteryType.NONE; - // device.battery_level = DeviceLevel.NONE; - // device.battery_percentage = 0.0f; + upower_device_removed(object_path); } /** @@ -252,25 +198,20 @@ class BluetoothClient : GLib.Object { * devices. */ private void upower_get_devices_cb(Object? obj, AsyncResult? res) { - GenericArray devices = null; - try { - devices = upower_client.get_devices_async.end(res); - } catch (Error e) { - warning("Error getting UPower devices: %s", e.message); - return; - } + GenericArray devices = upower_client.get_devices_async.end(res); - if (devices == null) { - warning("No UPower devices found"); - return; - } - - debug("Found %d UPower devices", devices.length); + if (devices == null) { + warning("No UPower devices found"); + return; + } - // Add each UPower device - foreach (var device in devices) { - upower_device_added_cb(device); + // Add each UPower device + foreach (var device in devices) { + upower_device_added_cb(device); + } + } catch (Error e) { + warning("Error getting UPower devices: %s", e.message); } } diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 60a03b035..d918e7b90 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -105,6 +105,26 @@ public class BluetoothIndicator : Bin { remove_device(device); }); + // Handle when a UPower device has been added + client.upower_device_added.connect((up_device) => { + devices_box.foreach((row) => { + var device_row = row as BTDeviceRow; + if (device_row.device.address == up_device.serial) { + device_row.up_device = up_device; + } + }); + }); + + // Handle when a UPower device has been removed + client.upower_device_removed.connect((path) => { + devices_box.foreach((row) => { + var device_row = row as BTDeviceRow; + if (((DBusProxy) device_row.device).get_object_path() == path) { + device_row.up_device = null; + } + }); + }); + client.global_state_changed.connect(on_client_state_changed); add(ebox); @@ -222,12 +242,27 @@ public class BTDeviceRow : ListBoxRow { private Image? image = null; private Label? name_label = null; private Label? status_label = null; + private Revealer? battery_revealer = null; + private Image? battery_icon = null; + private Label? battery_label = null; private Image? expand_icon = null; private Revealer? revealer = null; private Button? connection_button = null; public Device1 device { get; construct; } + private Up.Device? _up_device; + public Up.Device? up_device { + get { return _up_device; } + set { + _up_device = value; + _up_device.notify.connect(() => { + update_battery(); + }); + update_battery(); + } + } + public signal void properties_updated(); construct { @@ -258,6 +293,25 @@ public class BTDeviceRow : ListBoxRow { status_label.get_style_context().add_class("bluetooth-device-status"); status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); + battery_revealer = new Revealer() { + reveal_child = false, + transition_duration = 250, + transition_type = RevealerTransitionType.SLIDE_DOWN, + }; + + var battery_box = new Box(Orientation.HORIZONTAL, 0); + + battery_icon = new Image(); + battery_label = new Label(null) { + halign = START, + }; + battery_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); + + battery_box.pack_start(battery_label, false, false, 2); + battery_box.pack_start(battery_icon, false, false, 2); + + battery_revealer.add(battery_box); + expand_icon = new Image.from_icon_name("pan-end-symbolic", BUTTON); // Revealer stuff @@ -282,7 +336,8 @@ public class BTDeviceRow : ListBoxRow { grid.attach(image, 0, 0, 2, 2); grid.attach(name_label, 2, 0, 2, 1); grid.attach(status_label, 2, 1, 2, 1); - grid.attach(expand_icon, 4, 0, 2, 2); + grid.attach(battery_revealer, 2, 2, 1, 1); + grid.attach(expand_icon, 4, 0, 2, 3); box.pack_start(grid); box.pack_start(revealer); @@ -339,6 +394,57 @@ public class BTDeviceRow : ListBoxRow { } } + private void update_battery() { + if (up_device == null) { + battery_revealer.reveal_child = false; + return; + } + + string? fallback_icon_name = null; + string? icon_name = null; + + // round to nearest 10 + int rounded = (int) Math.round(up_device.percentage / 10) * 10; + + // Calculate our icon fallback if we don't have stepped battery icons + if (up_device.percentage <= 10) { + fallback_icon_name = "battery-empty"; + } else if (up_device.percentage <= 25) { + fallback_icon_name = "battery-critical"; + } else if (up_device.percentage <= 50) { + fallback_icon_name = "battery-low"; + } else if (up_device.percentage <= 75) { + fallback_icon_name = "battery-good"; + } else { + fallback_icon_name = "battery-full"; + } + + icon_name = "battery-level-%d".printf(rounded); + + // Fully charged or charging + if (up_device.state == 4) { + icon_name = "battery-full-charged"; + } else if (up_device.state == 1) { + icon_name += "-charging-symbolic"; + fallback_icon_name += "-charging-symbolic"; + } else { + icon_name += "-symbolic"; + } + + var theme = IconTheme.get_default(); + var icon_info = theme.lookup_icon(icon_name, IconSize.MENU, 0); + + if (icon_info == null) { + battery_icon.set_from_icon_name(fallback_icon_name, IconSize.MENU); + } else { + battery_icon.set_from_icon_name(icon_name, IconSize.MENU); + } + + battery_label.label = "%d%%".printf((int) up_device.percentage); + + battery_revealer.reveal_child = true; + } + private void update_status() { if (device.connected) { status_label.set_text(_("Connected")); From 6b3af298809f5eb9731c38239fefa56e84bd2864 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Fri, 10 Feb 2023 11:16:26 -0500 Subject: [PATCH 32/81] Use correct icon name for generic bluetooth items At least so far as there is a correct icon... Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index d918e7b90..1814848ee 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -274,7 +274,7 @@ public class BTDeviceRow : ListBoxRow { column_spacing = 6, }; - image = new Image.from_icon_name(device.icon ?? "bluetooth", LARGE_TOOLBAR); + image = new Image.from_icon_name(device.icon ?? "bluetooth-active", LARGE_TOOLBAR); image.get_style_context().add_class("bluetooth-device-image"); name_label = new Label(device.alias) { From 6c6f1fbaac593fe6c81881602354254e3d38a8f2 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 15 Feb 2023 17:43:13 -0500 Subject: [PATCH 33/81] Add styling to Bluetooth applet Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 18 ++++++++++------- src/theme/common/_bluetooth.scss | 20 +++++++++++++++++++ src/theme/common/_imports.scss | 1 + src/theme/meson.build | 1 + 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 src/theme/common/_bluetooth.scss diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 1814848ee..801ef7061 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -35,8 +35,8 @@ public class BluetoothIndicator : Bin { // Create our popover popover = new Budgie.Popover(ebox); - popover.set_size_request(200, -1); - popover.get_style_context().add_class("bluetooth-applet-popover"); + popover.set_size_request(250, -1); + popover.get_style_context().add_class("bluetooth-popover"); popover.hide.connect(() => { reset_revealers(); }); @@ -44,11 +44,12 @@ public class BluetoothIndicator : Bin { // Header var header = new Box(HORIZONTAL, 0); - header.get_style_context().add_class("bluetooth-popover-header"); + header.get_style_context().add_class("bluetooth-header"); // Header label var switch_label = new Label(_("Bluetooth")) { halign = START, + margin_start = 4, }; switch_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); @@ -73,8 +74,8 @@ public class BluetoothIndicator : Bin { // Devices var scrolled_window = new ScrolledWindow(null, null) { hscrollbar_policy = NEVER, - min_content_height = 250, - max_content_height = 250, + min_content_height = 275, + max_content_height = 275, propagate_natural_height = true }; devices_box = new ListBox() { @@ -161,7 +162,7 @@ public class BluetoothIndicator : Bin { try { app_info.launch(null, null); } catch (Error e) { - message("Unable to launch budgie-bluetooth-panel.desktop: %s", e.message); + warning("Unable to launch budgie-bluetooth-panel.desktop: %s", e.message); } } @@ -312,7 +313,9 @@ public class BTDeviceRow : ListBoxRow { battery_revealer.add(battery_box); - expand_icon = new Image.from_icon_name("pan-end-symbolic", BUTTON); + expand_icon = new Image.from_icon_name("pan-end-symbolic", BUTTON) { + margin_end = 12, // Add margin so the scrollbar doesn't overlap it + }; // Revealer stuff revealer = new Revealer() { @@ -324,6 +327,7 @@ public class BTDeviceRow : ListBoxRow { var revealer_body = new Box(HORIZONTAL, 0); connection_button = new Button.with_label(""); + connection_button.get_style_context().add_class(STYLE_CLASS_FLAT); connection_button.clicked.connect(on_connection_button_clicked); revealer_body.pack_start(connection_button); diff --git a/src/theme/common/_bluetooth.scss b/src/theme/common/_bluetooth.scss new file mode 100644 index 000000000..86b7bec95 --- /dev/null +++ b/src/theme/common/_bluetooth.scss @@ -0,0 +1,20 @@ +.bluetooth-popover { + .bluetooth-header { + > label { + font-weight: bold; + } + } + + .bluetooth-device-row { + padding-top: 6px; + padding-bottom: 6px; + + .bluetooth-device-name { + font-weight: bold; + } + + .bluetooth-device-status { + font-size: small; + } + } +} diff --git a/src/theme/common/_imports.scss b/src/theme/common/_imports.scss index 0187158cc..fecf03816 100644 --- a/src/theme/common/_imports.scss +++ b/src/theme/common/_imports.scss @@ -5,6 +5,7 @@ @import 'popover'; // Everything else +@import 'bluetooth'; @import 'borders'; @import 'dialogs'; @import 'drawing'; diff --git a/src/theme/meson.build b/src/theme/meson.build index a5430a3f5..5b293bcbb 100644 --- a/src/theme/meson.build +++ b/src/theme/meson.build @@ -21,6 +21,7 @@ theme_versions = [ ] sass_depend_files = files([ + 'common/_bluetooth.scss', 'common/_borders.scss', 'common/_colors.scss', 'common/_dialogs.scss', From d79ac2df541f43903a0237c7d9bc2bf950cb608a Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 18 Feb 2023 09:49:27 -0500 Subject: [PATCH 34/81] Redesign of Bluetooth device rows Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 148 ++++++++---------- src/theme/common/_bluetooth.scss | 24 ++- 2 files changed, 87 insertions(+), 85 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 801ef7061..d82ed4232 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -35,11 +35,8 @@ public class BluetoothIndicator : Bin { // Create our popover popover = new Budgie.Popover(ebox); - popover.set_size_request(250, -1); + popover.set_size_request(300, -1); popover.get_style_context().add_class("bluetooth-popover"); - popover.hide.connect(() => { - reset_revealers(); - }); var box = new Box(VERTICAL, 0); // Header @@ -74,8 +71,8 @@ public class BluetoothIndicator : Bin { // Devices var scrolled_window = new ScrolledWindow(null, null) { hscrollbar_policy = NEVER, - min_content_height = 275, - max_content_height = 275, + min_content_height = 250, + max_content_height = 250, propagate_natural_height = true }; devices_box = new ListBox() { @@ -86,8 +83,7 @@ public class BluetoothIndicator : Bin { devices_box.get_style_context().add_class("bluetooth-device-listbox"); devices_box.row_activated.connect((row) => { - var widget = row as BTDeviceRow; - widget.toggle_revealer(); + ((BTDeviceRow) row).try_connect_device(); }); scrolled_window.add(devices_box); @@ -221,19 +217,6 @@ public class BluetoothIndicator : Bin { private bool filter_paired_devices(ListBoxRow row) { return ((BTDeviceRow) row).device.paired || ((BTDeviceRow) row).device.connected; } - - /** - * Iterate over all devices in the list box and closes any open - * revealers. - */ - private void reset_revealers() { - devices_box.foreach((row) => { - var widget = row as BTDeviceRow; - if (widget.revealer_showing()) { - widget.toggle_revealer(); - } - }); - } } /** @@ -242,12 +225,12 @@ public class BluetoothIndicator : Bin { public class BTDeviceRow : ListBoxRow { private Image? image = null; private Label? name_label = null; - private Label? status_label = null; private Revealer? battery_revealer = null; private Image? battery_icon = null; private Label? battery_label = null; - private Image? expand_icon = null; private Revealer? revealer = null; + private Spinner? spinner = null; + private Label? status_label = null; private Button? connection_button = null; public Device1 device { get; construct; } @@ -275,7 +258,9 @@ public class BTDeviceRow : ListBoxRow { column_spacing = 6, }; - image = new Image.from_icon_name(device.icon ?? "bluetooth-active", LARGE_TOOLBAR); + var icon_name = device.icon ?? "bluetooth-active"; + if (!icon_name.has_suffix("-symbolic")) icon_name += "-symbolic"; + image = new Image.from_icon_name(icon_name, MENU); image.get_style_context().add_class("bluetooth-device-image"); name_label = new Label(device.alias) { @@ -288,12 +273,6 @@ public class BTDeviceRow : ListBoxRow { }; name_label.get_style_context().add_class("bluetooth-device-name"); - status_label = new Label(null) { - halign = START, - }; - status_label.get_style_context().add_class("bluetooth-device-status"); - status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); - battery_revealer = new Revealer() { reveal_child = false, transition_duration = 250, @@ -307,16 +286,13 @@ public class BTDeviceRow : ListBoxRow { halign = START, }; battery_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); + battery_label.get_style_context().add_class("bluetooth-battery-label"); battery_box.pack_start(battery_label, false, false, 2); battery_box.pack_start(battery_icon, false, false, 2); battery_revealer.add(battery_box); - expand_icon = new Image.from_icon_name("pan-end-symbolic", BUTTON) { - margin_end = 12, // Add margin so the scrollbar doesn't overlap it - }; - // Revealer stuff revealer = new Revealer() { reveal_child = false, @@ -326,76 +302,92 @@ public class BTDeviceRow : ListBoxRow { revealer.get_style_context().add_class("bluetooth-device-revealer"); var revealer_body = new Box(HORIZONTAL, 0); - connection_button = new Button.with_label(""); - connection_button.get_style_context().add_class(STYLE_CLASS_FLAT); - connection_button.clicked.connect(on_connection_button_clicked); - revealer_body.pack_start(connection_button); + spinner = new Spinner(); + status_label = new Label(null) { + halign = START, + margin_start = 6, + }; + status_label.get_style_context().add_class("bluetooth-device-status"); + status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); + + revealer_body.pack_start(spinner, false, false, 0); + revealer_body.pack_start(status_label); revealer.add(revealer_body); + // Diconnect button + connection_button = new Button.with_label(_("Disconnect")); + connection_button.get_style_context().add_class(STYLE_CLASS_FLAT); + connection_button.get_style_context().add_class("bluetooth-connection-button"); + connection_button.clicked.connect(on_connection_button_clicked); + // Signals ((DBusProxy) device).g_properties_changed.connect(update_status); // Packing grid.attach(image, 0, 0, 2, 2); grid.attach(name_label, 2, 0, 2, 1); - grid.attach(status_label, 2, 1, 2, 1); + grid.attach(connection_button, 4, 0, 1, 1); grid.attach(battery_revealer, 2, 2, 1, 1); - grid.attach(expand_icon, 4, 0, 2, 3); + grid.attach(revealer, 2, 3, 1, 1); box.pack_start(grid); - box.pack_start(revealer); add(box); update_status(); show_all(); + if (!device.connected) connection_button.hide(); } public BTDeviceRow(Device1 device) { Object(device: device); } - public bool revealer_showing() { - return revealer.reveal_child; - } + /** + * Try to connect to the Bluetooth device. + */ + public void try_connect_device() { + if (device.connected) return; + if (spinner.active) return; + + spinner.start(); + status_label.label = _("Connecting…"); + revealer.reveal_child = true; + + device.connect.begin((obj, res) => { + try { + device.connect.end(res); + connection_button.show(); + activatable = false; + } catch (Error e) { + warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); + } - public void toggle_revealer() { - revealer.reveal_child = !revealer.reveal_child; - if (revealer.reveal_child) { - expand_icon.set_from_icon_name("pan-down-symbolic", BUTTON); - } else { - expand_icon.set_from_icon_name("pan-end-symbolic", BUTTON); - } + revealer.reveal_child = false; + spinner.stop(); + }); } private void on_connection_button_clicked() { - connection_button.sensitive = false; - - if (device.connected) { // Device is connected; disconnect it - device.disconnect.begin((obj, res) => { - try { - device.disconnect.end(res); - - toggle_revealer(); - } catch (Error e) { - warning("Failed to disconnect Bluetooth device %s: %s", device.alias, e.message); - } - - connection_button.sensitive = true; - }); - } else if (!device.connected) { // Device isn't connected; connect it - device.connect.begin((obj, res) => { - try { - device.connect.end(res); - - toggle_revealer(); - } catch (Error e) { - warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); - } + if (!device.connected) return; + if (spinner.active) return; + + spinner.start(); + status_label.label = _("Disconnecting…"); + revealer.reveal_child = true; + + device.disconnect.begin((obj, res) => { + try { + device.disconnect.end(res); + connection_button.hide(); + activatable = true; + } catch (Error e) { + warning("Failed to disconnect Bluetooth device %s: %s", device.alias, e.message); + } - connection_button.sensitive = true; - }); - } + revealer.reveal_child = false; + spinner.stop(); + }); } private void update_battery() { @@ -452,10 +444,8 @@ public class BTDeviceRow : ListBoxRow { private void update_status() { if (device.connected) { status_label.set_text(_("Connected")); - connection_button.label = _("Disconnect"); } else { status_label.set_text(_("Disconnected")); - connection_button.label = _("Connect"); } properties_updated(); diff --git a/src/theme/common/_bluetooth.scss b/src/theme/common/_bluetooth.scss index 86b7bec95..0e6d4b6b8 100644 --- a/src/theme/common/_bluetooth.scss +++ b/src/theme/common/_bluetooth.scss @@ -1,20 +1,32 @@ .bluetooth-popover { .bluetooth-header { + padding-left: 3px; + padding-right: 6px; + > label { font-weight: bold; } } .bluetooth-device-row { - padding-top: 6px; - padding-bottom: 6px; + padding-left: 6px; + padding-right: 6px; + padding-top: 8px; + padding-bottom: 8px; - .bluetooth-device-name { - font-weight: bold; + .bluetooth-battery-label { + font-size: small; } - .bluetooth-device-status { - font-size: small; + .bluetooth-device-revealer { + padding-top: 4px; + } + + .bluetooth-connection-button { + & label { + font-size: small; + opacity: 0.55; // Emulate .dim-label + } } } } From 90911d917b9511466d3c33dad198c70b092035a9 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 18 Feb 2023 09:56:43 -0500 Subject: [PATCH 35/81] Update the name of a Bluetooth device if it changes Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index d82ed4232..761252825 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -448,6 +448,12 @@ public class BTDeviceRow : ListBoxRow { status_label.set_text(_("Disconnected")); } + // Update the name if changed + if (device.alias != name_label.label) { + name_label.label = device.alias; + name_label.tooltip_text = device.alias; + } + properties_updated(); } } From b63b435702f7578157470cf10135b9b1a91ccd16 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 19 Feb 2023 10:19:17 -0500 Subject: [PATCH 36/81] Add a placeholder widget if there are no Bluetooth devices Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 761252825..62fa0f230 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -86,6 +86,26 @@ public class BluetoothIndicator : Bin { ((BTDeviceRow) row).try_connect_device(); }); + // Placeholder + var placeholder = new Box(Orientation.VERTICAL, 18) { + margin_top = 18, + }; + var placeholder_label = new Label(_("No paired Bluetooth devices found.\n\nVisit Bluetooth settings to pair a device.")) { + justify = CENTER, + }; + placeholder_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); + placeholder_label.get_style_context().add_class("bluetooth-placeholder"); + + var placeholder_button = new Button.with_label(_("Open Bluetooth Settings")) { + relief = HALF, + }; + placeholder_button.get_style_context().add_class(STYLE_CLASS_SUGGESTED_ACTION); + placeholder_button.clicked.connect(on_settings_activate); + + placeholder.pack_start(placeholder_label, false); + placeholder.pack_start(placeholder_button, false); + placeholder.show_all(); // Without this, it never shows. Because... reasons? + devices_box.set_placeholder(placeholder); scrolled_window.add(devices_box); // Create our Bluetooth client @@ -315,7 +335,7 @@ public class BTDeviceRow : ListBoxRow { revealer_body.pack_start(status_label); revealer.add(revealer_body); - // Diconnect button + // Disconnect button connection_button = new Button.with_label(_("Disconnect")); connection_button.get_style_context().add_class(STYLE_CLASS_FLAT); connection_button.get_style_context().add_class("bluetooth-connection-button"); From 408168eaeec86a82bbdb3cc3d034f6a23542f654 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 19 Feb 2023 10:36:39 -0500 Subject: [PATCH 37/81] Make setting a new UPower device more robust Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 62fa0f230..9fe4cde1a 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -255,12 +255,22 @@ public class BTDeviceRow : ListBoxRow { public Device1 device { get; construct; } + private ulong up_handler_id = 0; private Up.Device? _up_device; public Up.Device? up_device { get { return _up_device; } set { + // Disconnect previous signal handler + if (up_handler_id != 0) { + _up_device.disconnect(up_handler_id); + up_handler_id = 0; + } + // Set new UPower device _up_device = value; - _up_device.notify.connect(() => { + // Connect to signal and update state if the new device + // isn't null + if (_up_device == null) return; + up_handler_id = _up_device.notify.connect(() => { update_battery(); }); update_battery(); From 9ea5e6c008f420b23338485ce3bc9a6c10b6dc1a Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 19 Feb 2023 10:40:57 -0500 Subject: [PATCH 38/81] Updating the battery state when the UPower device is null closes the revealer We want to do this, so put it above the null check. Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 9fe4cde1a..963afeb1d 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -267,13 +267,12 @@ public class BTDeviceRow : ListBoxRow { } // Set new UPower device _up_device = value; - // Connect to signal and update state if the new device - // isn't null + update_battery(); + // Connect to signal if the new device isn't null if (_up_device == null) return; up_handler_id = _up_device.notify.connect(() => { update_battery(); }); - update_battery(); } } From 07cd3afb007e5c877f129df06c2c9be3f9bc606d Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 19 Feb 2023 10:49:24 -0500 Subject: [PATCH 39/81] Don't rely on theme to pad revealer Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 3 ++- src/theme/common/_bluetooth.scss | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 963afeb1d..161c7f656 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -326,7 +326,8 @@ public class BTDeviceRow : ListBoxRow { revealer = new Revealer() { reveal_child = false, transition_duration = 250, - transition_type = RevealerTransitionType.SLIDE_DOWN + transition_type = RevealerTransitionType.SLIDE_DOWN, + margin_top = 4, }; revealer.get_style_context().add_class("bluetooth-device-revealer"); diff --git a/src/theme/common/_bluetooth.scss b/src/theme/common/_bluetooth.scss index 0e6d4b6b8..17dc39c87 100644 --- a/src/theme/common/_bluetooth.scss +++ b/src/theme/common/_bluetooth.scss @@ -18,10 +18,6 @@ font-size: small; } - .bluetooth-device-revealer { - padding-top: 4px; - } - .bluetooth-connection-button { & label { font-size: small; From 9a422a28cafb5c87b50737ee01e8c75f8081ba06 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sun, 19 Feb 2023 11:34:24 -0500 Subject: [PATCH 40/81] Refine dis/connection code flow Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 64 ++++++++----------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 161c7f656..3f3cfff65 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -83,7 +83,7 @@ public class BluetoothIndicator : Bin { devices_box.get_style_context().add_class("bluetooth-device-listbox"); devices_box.row_activated.connect((row) => { - ((BTDeviceRow) row).try_connect_device(); + ((BTDeviceRow) row).toggle_connection.begin(); }); // Placeholder @@ -331,12 +331,11 @@ public class BTDeviceRow : ListBoxRow { }; revealer.get_style_context().add_class("bluetooth-device-revealer"); - var revealer_body = new Box(HORIZONTAL, 0); + var revealer_body = new Box(HORIZONTAL, 6); spinner = new Spinner(); status_label = new Label(null) { halign = START, - margin_start = 6, }; status_label.get_style_context().add_class("bluetooth-device-status"); status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); @@ -349,7 +348,9 @@ public class BTDeviceRow : ListBoxRow { connection_button = new Button.with_label(_("Disconnect")); connection_button.get_style_context().add_class(STYLE_CLASS_FLAT); connection_button.get_style_context().add_class("bluetooth-connection-button"); - connection_button.clicked.connect(on_connection_button_clicked); + connection_button.clicked.connect(() => { + toggle_connection.begin(); + }); // Signals ((DBusProxy) device).g_properties_changed.connect(update_status); @@ -374,50 +375,35 @@ public class BTDeviceRow : ListBoxRow { } /** - * Try to connect to the Bluetooth device. + * Attempts to either connect to or disconnect from the Bluetooth + * device depending on its current connection state. */ - public void try_connect_device() { - if (device.connected) return; - if (spinner.active) return; - - spinner.start(); - status_label.label = _("Connecting…"); - revealer.reveal_child = true; - - device.connect.begin((obj, res) => { - try { - device.connect.end(res); - connection_button.show(); - activatable = false; - } catch (Error e) { - warning("Failed to connect to Bluetooth device %s: %s", device.alias, e.message); - } - - revealer.reveal_child = false; - spinner.stop(); - }); - } - - private void on_connection_button_clicked() { - if (!device.connected) return; + public async void toggle_connection() { if (spinner.active) return; - spinner.start(); - status_label.label = _("Disconnecting…"); + spinner.active = true; revealer.reveal_child = true; - device.disconnect.begin((obj, res) => { - try { - device.disconnect.end(res); + try { + if (device.connected) { + status_label.label = _("Disconnecting…"); + yield device.disconnect(); connection_button.hide(); activatable = true; - } catch (Error e) { - warning("Failed to disconnect Bluetooth device %s: %s", device.alias, e.message); + revealer.reveal_child = false; + } else { + status_label.label = _("Connecting…"); + yield device.connect(); + connection_button.show(); + activatable = false; + revealer.reveal_child = false; } + } catch (Error e) { + warning("Failed to connect or disconnect Bluetooth device %s: %s", device.alias, e.message); + status_label.label = device.connected ? _("Failed to disconnect") : _("Failed to connect"); + } - revealer.reveal_child = false; - spinner.stop(); - }); + spinner.active = false; } private void update_battery() { From ac0dded247296275ce3761e010ce6987555610cc Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 21 Feb 2023 13:53:04 -0500 Subject: [PATCH 41/81] Show or hide the panel widget based on if a Bluetooth adapter is present Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 2 ++ src/panel/applets/status/BluetoothIndicator.vala | 12 +++++++++++- src/panel/applets/status/StatusApplet.vala | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 8c2ec8719..1a0f65f56 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -141,6 +141,8 @@ class BluetoothClient : GLib.Object { if (powered == null) return; set_last_powered.begin(); }); + + has_adapter = true; } else if (iface is Device1) { unowned Device1 device = iface as Device1; device_added(device); diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 3f3cfff65..8d4dc7983 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -144,6 +144,12 @@ public class BluetoothIndicator : Bin { client.global_state_changed.connect(on_client_state_changed); + // Show or hide the panel widget if we have a Bluetooth adapter or not + client.notify["has-adapter"].connect(() => { + if (client.has_adapter) show_all(); + else hide(); + }); + add(ebox); box.pack_start(header); box.pack_start(new Separator(HORIZONTAL), true, true, 2); @@ -151,7 +157,11 @@ public class BluetoothIndicator : Bin { box.pack_start(new Separator(HORIZONTAL), true, true, 2); box.show_all(); popover.add(box); - show_all(); + + // Only show if we have an adapter present + if (client.has_adapter) { + show_all(); + } } private bool on_button_released(EventButton e) { diff --git a/src/panel/applets/status/StatusApplet.vala b/src/panel/applets/status/StatusApplet.vala index 8218578f5..75e5f4f08 100644 --- a/src/panel/applets/status/StatusApplet.vala +++ b/src/panel/applets/status/StatusApplet.vala @@ -106,7 +106,7 @@ public class StatusApplet : Budgie.Applet { blue = new BluetoothIndicator(); widget.pack_start(blue, false, false, 0); - blue.show_all(); + /* Bluetooth widget shows itself - we dont control that */ this.setup_popover(blue.ebox, blue.popover); } From 2dbadbd1d979f7e3cf14602f80f3e870234b2179 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 21 Feb 2023 13:58:05 -0500 Subject: [PATCH 42/81] Remove extra separator in Bluetooth popover Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 8d4dc7983..0590ed90a 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -154,7 +154,6 @@ public class BluetoothIndicator : Bin { box.pack_start(header); box.pack_start(new Separator(HORIZONTAL), true, true, 2); box.pack_start(scrolled_window); - box.pack_start(new Separator(HORIZONTAL), true, true, 2); box.show_all(); popover.add(box); From 869564d3fe8f7e2fd4a6962e921d825920ebfd4d Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 21 Feb 2023 14:06:13 -0500 Subject: [PATCH 43/81] Format style change Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 0590ed90a..a716bceeb 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -158,9 +158,7 @@ public class BluetoothIndicator : Bin { popover.add(box); // Only show if we have an adapter present - if (client.has_adapter) { - show_all(); - } + if (client.has_adapter) show_all(); } private bool on_button_released(EventButton e) { From cb4bb9a92754138d59b8c8b5fa45f6ae03dc5f07 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 22 Feb 2023 10:40:52 -0500 Subject: [PATCH 44/81] Ensure that state hinging on connection status is always updated Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index a716bceeb..5a0df1404 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -395,14 +395,10 @@ public class BTDeviceRow : ListBoxRow { if (device.connected) { status_label.label = _("Disconnecting…"); yield device.disconnect(); - connection_button.hide(); - activatable = true; revealer.reveal_child = false; } else { status_label.label = _("Connecting…"); yield device.connect(); - connection_button.show(); - activatable = false; revealer.reveal_child = false; } } catch (Error e) { @@ -467,8 +463,12 @@ public class BTDeviceRow : ListBoxRow { private void update_status() { if (device.connected) { status_label.set_text(_("Connected")); + connection_button.show(); + activatable = false; } else { status_label.set_text(_("Disconnected")); + connection_button.hide(); + activatable = true; } // Update the name if changed From 467ec478e06c83d29fdaeda3f9dc54972fe52535 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 22 Feb 2023 10:43:20 -0500 Subject: [PATCH 45/81] Make things less embiggened Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 5a0df1404..c57441871 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -35,7 +35,7 @@ public class BluetoothIndicator : Bin { // Create our popover popover = new Budgie.Popover(ebox); - popover.set_size_request(300, -1); + popover.set_size_request(275, -1); popover.get_style_context().add_class("bluetooth-popover"); var box = new Box(VERTICAL, 0); @@ -71,8 +71,8 @@ public class BluetoothIndicator : Bin { // Devices var scrolled_window = new ScrolledWindow(null, null) { hscrollbar_policy = NEVER, - min_content_height = 250, - max_content_height = 250, + min_content_height = 190, + max_content_height = 190, propagate_natural_height = true }; devices_box = new ListBox() { From e821bb9656bf7fa1527558e3860e444b0506ea76 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 27 Feb 2023 15:53:50 -0500 Subject: [PATCH 46/81] Refine battery display for new device row layout Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index c57441871..be03e4d73 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -313,6 +313,7 @@ public class BTDeviceRow : ListBoxRow { reveal_child = false, transition_duration = 250, transition_type = RevealerTransitionType.SLIDE_DOWN, + margin_top = 2, }; var battery_box = new Box(Orientation.HORIZONTAL, 0); @@ -324,8 +325,8 @@ public class BTDeviceRow : ListBoxRow { battery_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); battery_label.get_style_context().add_class("bluetooth-battery-label"); - battery_box.pack_start(battery_label, false, false, 2); battery_box.pack_start(battery_icon, false, false, 2); + battery_box.pack_start(battery_label, false, false, 2); battery_revealer.add(battery_box); @@ -425,7 +426,7 @@ public class BTDeviceRow : ListBoxRow { if (up_device.percentage <= 10) { fallback_icon_name = "battery-empty"; } else if (up_device.percentage <= 25) { - fallback_icon_name = "battery-critical"; + fallback_icon_name = "battery-caution"; } else if (up_device.percentage <= 50) { fallback_icon_name = "battery-low"; } else if (up_device.percentage <= 75) { From 22e0097621f1a0916defbd780375cbe07c9299ba Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 27 Feb 2023 16:13:18 -0500 Subject: [PATCH 47/81] Update the tray icon when Bluetooth is enabled or disabled Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index be03e4d73..d596640b2 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -26,7 +26,7 @@ public class BluetoothIndicator : Bin { private BluetoothClient client; construct { - image = new Image.from_icon_name("bluetooth-active-symbolic", IconSize.MENU); + image = new Image(); ebox = new EventBox(); ebox.add(image); @@ -150,6 +150,9 @@ public class BluetoothIndicator : Bin { else hide(); }); + // Make sure our starting icon is correct + update_tray_icon(); + add(ebox); box.pack_start(header); box.pack_start(new Separator(HORIZONTAL), true, true, 2); @@ -193,6 +196,7 @@ public class BluetoothIndicator : Bin { // Turn Bluetooth on or off client.set_all_powered.begin(bluetooth_switch.active, (obj, res) => { client.check_powered(); + update_tray_icon(); }); } @@ -244,6 +248,17 @@ public class BluetoothIndicator : Bin { private bool filter_paired_devices(ListBoxRow row) { return ((BTDeviceRow) row).device.paired || ((BTDeviceRow) row).device.connected; } + + /** + * Update the tray icon used depending on the current Bluetooth state. + */ + private void update_tray_icon() { + if (bluetooth_switch.active) { + image.set_from_icon_name("bluetooth-active", IconSize.MENU); + } else { + image.set_from_icon_name("bluetooth-disabled", IconSize.MENU); + } + } } /** From b1a4ee889640e8ccbf4da91d15ab03f1e1a288e5 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Mar 2023 10:24:35 -0500 Subject: [PATCH 48/81] bluetooth-indicator: Invalidate the device filter whenever we invalidate the sorting Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index d596640b2..7bba52a04 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -207,10 +207,12 @@ public class BluetoothIndicator : Bin { widget.properties_updated.connect(() => { client.check_powered(); + devices_box.invalidate_filter(); devices_box.invalidate_sort(); }); devices_box.add(widget); + devices_box.invalidate_filter(); devices_box.invalidate_sort(); } @@ -224,6 +226,7 @@ public class BluetoothIndicator : Bin { } }); + devices_box.invalidate_filter(); devices_box.invalidate_sort(); } From c9ab2a74f17103b099b4f4328b99df2db2fc57f2 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 7 Mar 2023 13:01:23 -0500 Subject: [PATCH 49/81] bluetooth-indicator: Final (I hope) design edit to the device row layout Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 23 +++++++++---------- src/theme/common/_bluetooth.scss | 7 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 7bba52a04..e1a00ee86 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -348,27 +348,27 @@ public class BTDeviceRow : ListBoxRow { battery_revealer.add(battery_box); - // Revealer stuff + // Status area stuff + var status_box = new Box(HORIZONTAL, 6); + revealer = new Revealer() { reveal_child = false, transition_duration = 250, - transition_type = RevealerTransitionType.SLIDE_DOWN, - margin_top = 4, + transition_type = RevealerTransitionType.CROSSFADE, }; revealer.get_style_context().add_class("bluetooth-device-revealer"); - var revealer_body = new Box(HORIZONTAL, 6); - spinner = new Spinner(); + status_label = new Label(null) { halign = START, }; status_label.get_style_context().add_class("bluetooth-device-status"); status_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); - revealer_body.pack_start(spinner, false, false, 0); - revealer_body.pack_start(status_label); - revealer.add(revealer_body); + revealer.add(spinner); + status_box.pack_start(status_label, false); + status_box.pack_start(revealer, false); // Disconnect button connection_button = new Button.with_label(_("Disconnect")); @@ -384,9 +384,9 @@ public class BTDeviceRow : ListBoxRow { // Packing grid.attach(image, 0, 0, 2, 2); grid.attach(name_label, 2, 0, 2, 1); - grid.attach(connection_button, 4, 0, 1, 1); + grid.attach(connection_button, 4, 0, 1, 2); + grid.attach(status_box, 2, 1, 2, 1); grid.attach(battery_revealer, 2, 2, 1, 1); - grid.attach(revealer, 2, 3, 1, 1); box.pack_start(grid); add(box); @@ -414,17 +414,16 @@ public class BTDeviceRow : ListBoxRow { if (device.connected) { status_label.label = _("Disconnecting…"); yield device.disconnect(); - revealer.reveal_child = false; } else { status_label.label = _("Connecting…"); yield device.connect(); - revealer.reveal_child = false; } } catch (Error e) { warning("Failed to connect or disconnect Bluetooth device %s: %s", device.alias, e.message); status_label.label = device.connected ? _("Failed to disconnect") : _("Failed to connect"); } + revealer.reveal_child = true; spinner.active = false; } diff --git a/src/theme/common/_bluetooth.scss b/src/theme/common/_bluetooth.scss index 17dc39c87..fee149304 100644 --- a/src/theme/common/_bluetooth.scss +++ b/src/theme/common/_bluetooth.scss @@ -11,10 +11,11 @@ .bluetooth-device-row { padding-left: 6px; padding-right: 6px; - padding-top: 8px; - padding-bottom: 8px; + padding-top: 2px; + padding-bottom: 2px; - .bluetooth-battery-label { + .bluetooth-battery-label, + .bluetooth-device-status { font-size: small; } From 8bfae77c1c933def75dbeb8e13b10339b115612e Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 11 Mar 2023 16:34:44 -0500 Subject: [PATCH 50/81] bluetooth-indicator: Use an icon button for the disconnect button Also fixes some spacing issues when not using built-in theme. Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 19 ++++++++++++++----- src/theme/common/_bluetooth.scss | 5 ----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index e1a00ee86..53cde6dd3 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -40,7 +40,10 @@ public class BluetoothIndicator : Bin { var box = new Box(VERTICAL, 0); // Header - var header = new Box(HORIZONTAL, 0); + var header = new Box(HORIZONTAL, 0) { + margin_start = 4, + margin_end = 4, + }; header.get_style_context().add_class("bluetooth-header"); // Header label @@ -60,7 +63,7 @@ public class BluetoothIndicator : Bin { // Bluetooth switch bluetooth_switch = new Switch() { - tooltip_text = _("Turn Bluetooth on or off") + tooltip_text = _("Turn Bluetooth on or off"), }; bluetooth_switch.notify["active"].connect(on_switch_activate); @@ -314,7 +317,10 @@ public class BTDeviceRow : ListBoxRow { var icon_name = device.icon ?? "bluetooth-active"; if (!icon_name.has_suffix("-symbolic")) icon_name += "-symbolic"; - image = new Image.from_icon_name(icon_name, MENU); + image = new Image.from_icon_name(icon_name, MENU) { + margin_start = 4, + margin_end = 4, + }; image.get_style_context().add_class("bluetooth-device-image"); name_label = new Label(device.alias) { @@ -371,8 +377,11 @@ public class BTDeviceRow : ListBoxRow { status_box.pack_start(revealer, false); // Disconnect button - connection_button = new Button.with_label(_("Disconnect")); - connection_button.get_style_context().add_class(STYLE_CLASS_FLAT); + connection_button = new Button.from_icon_name("bluetooth-disabled-symbolic", IconSize.BUTTON) { + relief = ReliefStyle.HALF, + tooltip_text = _("Disconnect"), + }; + connection_button.get_style_context().add_class("circular"); connection_button.get_style_context().add_class("bluetooth-connection-button"); connection_button.clicked.connect(() => { toggle_connection.begin(); diff --git a/src/theme/common/_bluetooth.scss b/src/theme/common/_bluetooth.scss index fee149304..5ca2fcb88 100644 --- a/src/theme/common/_bluetooth.scss +++ b/src/theme/common/_bluetooth.scss @@ -1,16 +1,11 @@ .bluetooth-popover { .bluetooth-header { - padding-left: 3px; - padding-right: 6px; - > label { font-weight: bold; } } .bluetooth-device-row { - padding-left: 6px; - padding-right: 6px; padding-top: 2px; padding-bottom: 2px; From b1baa1acabb611c82936c7df33c9b71387738e43 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 14 Mar 2023 10:02:53 -0400 Subject: [PATCH 51/81] [WIP] bluetooth-indicator: Implement support for file sending Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothDBus.vala | 23 ++++ .../applets/status/BluetoothIndicator.vala | 124 +++++++++++++++++- .../applets/status/BluetoothObexManager.vala | 108 +++++++++++++++ src/panel/applets/status/meson.build | 1 + src/theme/common/_bluetooth.scss | 7 - 5 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 src/panel/applets/status/BluetoothObexManager.vala diff --git a/src/panel/applets/status/BluetoothDBus.vala b/src/panel/applets/status/BluetoothDBus.vala index f1a09578c..31df6b9ef 100644 --- a/src/panel/applets/status/BluetoothDBus.vala +++ b/src/panel/applets/status/BluetoothDBus.vala @@ -63,3 +63,26 @@ public interface Device1 : GLib.Object { public async abstract void pair() throws GLib.DBusError, GLib.IOError; public async abstract void cancel_pairing() throws GLib.DBusError, GLib.IOError; } + +[DBus (name="org.bluez.obex.Transfer1")] +public interface Transfer : GLib.Object { + public abstract string status { owned get; } + public abstract ObjectPath session { owned get; } + public abstract string name { owned get; } + public abstract string Type { owned get; } + public abstract uint64 time { owned get; } + public abstract uint64 size { owned get; } + public abstract uint64 transferred { owned get; } + public abstract string filename { owned get; } + + public abstract void cancel() throws GLib.DBusError, GLib.Error; +} + +[DBus (name="org.bluez.obex.Session1")] +public interface Session : GLib.Object { + public abstract string source { owned get; } + public abstract string destination { owned get; } + public abstract uchar channel { owned get; } + public abstract string target { owned get; } + public abstract string root { owned get; } +} diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 53cde6dd3..ab782e2b6 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -279,9 +279,15 @@ public class BTDeviceRow : ListBoxRow { private Revealer? revealer = null; private Spinner? spinner = null; private Label? status_label = null; + private Button? send_file_button = null; private Button? connection_button = null; + private Revealer? progress_revealer = null; + private Label? file_label = null; + private Label? progress_label = null; + private ProgressBar? progress_bar = null; public Device1 device { get; construct; } + public Transfer transfer; private ulong up_handler_id = 0; private Up.Device? _up_device; @@ -309,6 +315,12 @@ public class BTDeviceRow : ListBoxRow { construct { get_style_context().add_class("bluetooth-device-row"); + // Obex manager for file transfers + var obex_manager = new ObexManager(); + obex_manager.transfer_active.connect(transfer_active); + obex_manager.transfer_added.connect(transfer_added); + obex_manager.transfer_removed.connect(transfer_removed); + // Body var box = new Box(Orientation.VERTICAL, 0); var grid = new Grid() { @@ -376,6 +388,8 @@ public class BTDeviceRow : ListBoxRow { status_box.pack_start(status_label, false); status_box.pack_start(revealer, false); + var button_box = new Box(Orientation.HORIZONTAL, 0); + // Disconnect button connection_button = new Button.from_icon_name("bluetooth-disabled-symbolic", IconSize.BUTTON) { relief = ReliefStyle.HALF, @@ -387,28 +401,77 @@ public class BTDeviceRow : ListBoxRow { toggle_connection.begin(); }); + // Send file button + send_file_button = new Button.from_icon_name("folder-download-symbolic", IconSize.BUTTON) { + relief = ReliefStyle.HALF, + tooltip_text = _("Send file…"), + }; + send_file_button.clicked.connect(() => { + + }); + send_file_button.get_style_context().add_class("circular"); + + button_box.pack_start(send_file_button, false); + button_box.pack_start(connection_button, false); + + // Progress stuff + progress_revealer = new Revealer() { + reveal_child = false, + transition_duration = 250, + transition_type = RevealerTransitionType.SLIDE_DOWN, + }; + + progress_label = new Label(null) { + halign = Align.START, + valign = Align.END, + use_markup = true, + hexpand = true, + }; + + progress_bar = new ProgressBar() { + hexpand = true, + }; + + file_label = new Label(null) { + ellipsize = Pango.EllipsizeMode.MIDDLE, + halign = Align.START, + valign = Align.END, + use_markup = true, + hexpand = true, + }; + + var progress_grid = new Grid(); + progress_grid.attach(file_label, 0, 0); + progress_grid.attach(progress_bar, 0, 1); + progress_grid.attach(progress_label, 0, 2); + // Signals ((DBusProxy) device).g_properties_changed.connect(update_status); // Packing grid.attach(image, 0, 0, 2, 2); grid.attach(name_label, 2, 0, 2, 1); - grid.attach(connection_button, 4, 0, 1, 2); + grid.attach(button_box, 4, 0, 1, 2); grid.attach(status_box, 2, 1, 2, 1); grid.attach(battery_revealer, 2, 2, 1, 1); + grid.attach(progress_revealer, 1, 3); box.pack_start(grid); add(box); - update_status(); show_all(); - if (!device.connected) connection_button.hide(); + update_status(); } public BTDeviceRow(Device1 device) { Object(device: device); } + private void hide_progress_revealer() { + progress_label.label = ""; + progress_revealer.reveal_child = false; + } + /** * Attempts to either connect to or disconnect from the Bluetooth * device depending on its current connection state. @@ -436,6 +499,18 @@ public class BTDeviceRow : ListBoxRow { spinner.active = false; } + private void transfer_active(string address) { + if (address == device.address) update_transfer_progress(); + } + + private void transfer_added(string address, Transfer transfer) { + if (address == device.address) this.transfer = transfer; + } + + private void transfer_removed(Transfer transfer) { + hide_progress_revealer(); + } + private void update_battery() { if (up_device == null) { battery_revealer.reveal_child = false; @@ -491,10 +566,12 @@ public class BTDeviceRow : ListBoxRow { if (device.connected) { status_label.set_text(_("Connected")); connection_button.show(); + send_file_button.show(); activatable = false; } else { status_label.set_text(_("Disconnected")); connection_button.hide(); + send_file_button.hide(); activatable = true; } @@ -506,4 +583,45 @@ public class BTDeviceRow : ListBoxRow { properties_updated(); } + + private void update_transfer_progress() { + switch (transfer.status) { + case "error": + hide_progress_revealer(); + break; + case "queued": + hide_progress_revealer(); + break; + case "active": + // Update the progress bar + progress_bar.fraction = (double) transfer.transferred / (double) transfer.size; + progress_revealer.reveal_child = true; + + // Update the filename label + var name = transfer.name; + if (name == null) { + file_label.label = _("Filename: %s").printf(Markup.escape_text(name)); + } + + // Update the progress label + var file_name = transfer.filename; + if (file_name != null) { + if (file_name.contains("/.cache/obexd")) { + progress_label.label = _("Receiving… %s of %s").printf( + format_size(transfer.transferred), + format_size(transfer.size) + ); + } else { + progress_label.label = _("Sending… %s of %s").printf( + format_size(transfer.transferred), + format_size(transfer.size) + ); + } + } + break; + case "complete": + hide_progress_revealer(); + break; + } + } } diff --git a/src/panel/applets/status/BluetoothObexManager.vala b/src/panel/applets/status/BluetoothObexManager.vala new file mode 100644 index 000000000..bbc7ef3f1 --- /dev/null +++ b/src/panel/applets/status/BluetoothObexManager.vala @@ -0,0 +1,108 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +using GLib; + +public class ObexManager : Object { + public signal void transfer_added(string address, Transfer transfer); + public signal void transfer_removed(Transfer transfer); + public signal void transfer_active(string address); + + private DBusObjectManager object_manager; + + construct { + create_manager.begin(); + } + + /** + * Creates our Obex DBus object manager and connects to its signals. + */ + private async void create_manager() { + try { + object_manager = yield new DBusObjectManagerClient.for_bus( + BusType.SESSION, + DBusObjectManagerClientFlags.NONE, + "org.bluez.obex", + "/", + object_manager_proxy_get_type + ); + + // Get and add any current Transfers + object_manager.get_objects().foreach((obj) => { + obj.get_interfaces().foreach((iface) => interface_added(obj, iface)); + }); + + // Connect signals for added/removed interfaces + object_manager.interface_added.connect(interface_added); + object_manager.interface_removed.connect(interface_removed); + + // Connect signals for added/removed objects + object_manager.object_added.connect((obj) => { + obj.get_interfaces().foreach((iface) => interface_added(obj, iface)); + }); + + object_manager.object_removed.connect((obj) => { + obj.get_interfaces().foreach((iface) => interface_removed(obj, iface)); + }); + } catch (Error e) { + critical("Error getting DBus object manager for Obex: %s", e.message); + } + } + + [CCode (cname="transfer_proxy_get_type")] + extern static Type get_obex_transfer_proxy_type(); + + /** + * Get the type for our object manager interfaces. + */ + private Type object_manager_proxy_get_type(DBusObjectManagerClient manager, string object_path, string? interface_name) { + if (interface_name == null) return typeof(DBusObjectProxy); + + if (interface_name == "org.bluez.obex.Transfer1") return get_obex_transfer_proxy_type(); + + return typeof(DBusProxy); + } + + /** + * Handles when an interface has been added. + */ + private void interface_added(DBusObject obj, DBusInterface iface) { + if (iface is Transfer) { + unowned Transfer transfer = iface as Transfer; + Session? session = null; + + try { + session = Bus.get_proxy_sync( + BusType.SESSION, + "org.bluez.obex", + transfer.session + ); + } catch (Error e) { + critical("Error getting Obex session proxy: %s", e.message); + } + + transfer_added(session.destination, transfer); + + ((DBusProxy) transfer).g_properties_changed.connect((changed, invalid) => { + transfer_active(session.destination); + }); + } + } + + /** + * Handles when an interface has been removed. + */ + private void interface_removed(DBusObject obj, DBusInterface iface) { + if (iface is Transfer) { + transfer_removed(iface as Transfer); + } + } +} diff --git a/src/panel/applets/status/meson.build b/src/panel/applets/status/meson.build index 7eacf8cb0..f51686a2c 100644 --- a/src/panel/applets/status/meson.build +++ b/src/panel/applets/status/meson.build @@ -23,6 +23,7 @@ applet_status_sources = [ 'BluetoothDBus.vala', 'BluetoothEnums.vala', 'BluetoothIndicator.vala', + 'BluetoothObexManager.vala', 'StatusApplet.vala', 'PowerIndicator.vala', 'SoundIndicator.vala', diff --git a/src/theme/common/_bluetooth.scss b/src/theme/common/_bluetooth.scss index 5ca2fcb88..366eb946e 100644 --- a/src/theme/common/_bluetooth.scss +++ b/src/theme/common/_bluetooth.scss @@ -13,12 +13,5 @@ .bluetooth-device-status { font-size: small; } - - .bluetooth-connection-button { - & label { - font-size: small; - opacity: 0.55; // Emulate .dim-label - } - } } } From f249729b7f063f5391468ab4391b1c6ff3a017c5 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 15 Mar 2023 19:47:49 -0400 Subject: [PATCH 52/81] bluetooth-indicator: Use rfkill to enable/disable Bluetooth Yes, the DBus interface for it is provided by Gnome Settings Daemon, but that's always going to be present anyways and this makes life way simpler. Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothClient.vala | 155 ++++-------------- src/panel/applets/status/BluetoothDBus.vala | 5 + .../applets/status/BluetoothIndicator.vala | 153 ++++++++++------- 3 files changed, 131 insertions(+), 182 deletions(-) diff --git a/src/panel/applets/status/BluetoothClient.vala b/src/panel/applets/status/BluetoothClient.vala index 1a0f65f56..ca508e901 100644 --- a/src/panel/applets/status/BluetoothClient.vala +++ b/src/panel/applets/status/BluetoothClient.vala @@ -20,17 +20,17 @@ const string BLUEZ_MANAGER_PATH = "/"; const string BLUEZ_ADAPTER_INTERFACE = "org.bluez.Adapter1"; const string BLUEZ_DEVICE_INTERFACE = "org.bluez.Device1"; const string BLUETOOTH_ADDRESS_PREFIX = "/org/bluez/"; +const string RFKILL_DBUS_NAME = "org.gnome.SettingsDaemon.Rfkill"; +const string RFKILL_DBUS_PATH = "/org/gnome/SettingsDaemon/Rfkill"; class BluetoothClient : GLib.Object { private Cancellable cancellable; private DBusObjectManagerClient object_manager; private Client upower_client; + private Rfkill rfkill; public bool has_adapter { get; private set; default = false; } - public bool is_connected { get; private set; default = false; } - public bool is_enabled { get; private set; default = false; } - public bool is_powered { get; private set; default = false; } public bool retrieve_finished { get; private set; default = false; } /** Signal emitted when a Bluetooth device has been added. */ @@ -41,12 +41,15 @@ class BluetoothClient : GLib.Object { public signal void upower_device_added(Up.Device up_device); /** Signal emitted when a UPower device for a Bluetooth device has been removed. */ public signal void upower_device_removed(string object_path); - /** Signal emitted when our powered or connected state changes. */ - public signal void global_state_changed(bool enabled, bool connected); + /** Signal emitted when airplane mode state has been changed. */ + public signal void airplane_mode_changed(); construct { cancellable = new Cancellable(); + // Get our RFKill proxy + create_rfkill_proxy(); + // Set up our UPower client create_upower_client.begin(); @@ -129,29 +132,35 @@ class BluetoothClient : GLib.Object { retrieve_finished = true; } + private void create_rfkill_proxy() { + try { + rfkill = Bus.get_proxy_sync( + BusType.SESSION, + RFKILL_DBUS_NAME, + RFKILL_DBUS_PATH, + DBusProxyFlags.NONE, + cancellable + ); + + ((DBusProxy) rfkill).g_properties_changed.connect((changed, invalid) => { + var variant = changed.lookup_value("BluetoothAirplaneMode", new VariantType("b")); + if (variant == null) return; + airplane_mode_changed(); + }); + } catch (Error e) { + critical("Error getting RFKill proxy: %s", e.message); + } + } + /** * Handles the addition of a DBus object interface. */ private void on_interface_added(DBusObject object, DBusInterface iface) { if (iface is Adapter1) { - unowned Adapter1 adapter = iface as Adapter1; - - ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { - var powered = changed.lookup_value("Powered", new VariantType("b")); - if (powered == null) return; - set_last_powered.begin(); - }); - has_adapter = true; } else if (iface is Device1) { unowned Device1 device = iface as Device1; device_added(device); - - ((DBusProxy) device).g_properties_changed.connect((changed, invalid) => { - check_powered(); - }); - - check_powered(); } } @@ -257,7 +266,7 @@ class BluetoothClient : GLib.Object { /** * Get all Bluetooth adapters from our Bluez object manager. */ - public List get_adapters() { + private List get_adapters() { var adapters = new List(); object_manager.get_objects().foreach((object) => { @@ -270,110 +279,16 @@ class BluetoothClient : GLib.Object { } /** - * Get all Bluetooth devices from our Bluez object manager. - */ - public List get_devices() { - var devices = new List(); - - object_manager.get_objects().foreach((object) => { - var iface = object.get_interface(BLUEZ_DEVICE_INTERFACE); - if (iface == null) return; - devices.append(iface as Device1); - }); - - return (owned) devices; - } - - /** - * Check if any adapter is currently connected. - */ - public bool get_connected() { - var devices = get_devices(); - - foreach (var device in devices) { - if (device.connected) return true; - } - - return false; - } - - /** - * Check if any adapter is powered on. + * Get whether or not Bluetooth airplane mode is enabled. */ - public bool get_powered() { - var adapters = get_adapters(); - - foreach (var adapter in adapters) { - if (adapter.powered) return true; - } - - return false; + public bool airplane_mode_enabled() { + return rfkill.bluetooth_airplane_mode; } /** - * Check if any Bluetooth adapter is powered and connected, and update our - * Bluetooth state accordingly. + * Set whether or not Bluetooth airplane mode is enabled. */ - public void check_powered() { - // This is called usually as a signal handler, so start an Idle - // task to prevent race conditions. - Idle.add(() => { - // Get current state - var connected = get_connected(); - var powered = get_powered(); - - debug("connected: %s new_connected: %s | powered: %s new_powered: %s", - is_connected ? "yes" : "no", connected ? "yes" : "no", - is_powered ? "yes" : "no", powered ? "yes" : "no" - ); - - // Do nothing if the state hasn't changed - if (connected == is_connected && powered == is_powered) return Source.REMOVE; - - // Set the new state - is_connected = connected; - is_powered = powered; - - // Emit changed signal - global_state_changed(powered, connected); - - return Source.REMOVE; - }); - } - - /** - * Set the powered state of all adapters. If being powered off and an adapter has - * devices connected to it, they will be disconnected. - * - * It is intended to use `check_powered()` as a callback to this async function. - * As such, this function does not set our global state directly. - */ - public async void set_all_powered(bool powered) { - // Set the adapters' powered state - var adapters = get_adapters(); - foreach (var adapter in adapters) { - adapter.powered = powered; - } - - is_enabled = powered; - - if (powered) return; - - // If the power is being turned off, disconnect from all devices - var devices = get_devices(); - foreach (var device in devices) { - if (device.connected) { - try { - yield device.disconnect(); - } catch (Error e) { - warning("Error disconnecting Bluetooth device: %s", e.message); - } - } - } - } - - public async void set_last_powered() { - yield set_all_powered(is_enabled); - check_powered(); + public void set_airplane_mode(bool enabled) { + rfkill.bluetooth_airplane_mode = enabled; } } diff --git a/src/panel/applets/status/BluetoothDBus.vala b/src/panel/applets/status/BluetoothDBus.vala index 31df6b9ef..cadc0b741 100644 --- a/src/panel/applets/status/BluetoothDBus.vala +++ b/src/panel/applets/status/BluetoothDBus.vala @@ -86,3 +86,8 @@ public interface Session : GLib.Object { public abstract string target { owned get; } public abstract string root { owned get; } } + +[DBus (name="org.gnome.SettingsDaemon.Rfkill")] +public interface Rfkill : GLib.Object { + public abstract bool bluetooth_airplane_mode { get; set; } +} diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index ab782e2b6..1084007e4 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -22,9 +22,13 @@ public class BluetoothIndicator : Bin { private ListBox? devices_box = null; private Switch? bluetooth_switch = null; + private Label? placeholder_label = null; + private Label? placeholder_sublabel = null; private BluetoothClient client; + private ulong switch_handler_id; + construct { image = new Image(); @@ -33,6 +37,49 @@ public class BluetoothIndicator : Bin { ebox.add_events(EventMask.BUTTON_RELEASE_MASK); ebox.button_release_event.connect(on_button_released); + // Create our Bluetooth client + client = new BluetoothClient(); + + client.device_added.connect((device) => { + // Remove any existing rows for this device + remove_device(device); + // Add the new device to correctly update its status + add_device(device); + }); + + client.device_removed.connect((device) => { + remove_device(device); + }); + + // Handle when a UPower device has been added + client.upower_device_added.connect((up_device) => { + devices_box.foreach((row) => { + var device_row = row as BTDeviceRow; + if (device_row.device.address == up_device.serial) { + device_row.up_device = up_device; + } + }); + }); + + // Handle when a UPower device has been removed + client.upower_device_removed.connect((path) => { + devices_box.foreach((row) => { + var device_row = row as BTDeviceRow; + if (((DBusProxy) device_row.device).get_object_path() == path) { + device_row.up_device = null; + } + }); + }); + + // Handle changes to airplane mode + client.airplane_mode_changed.connect(update_state_ui); + + // Show or hide the panel widget if we have a Bluetooth adapter or not + client.notify["has-adapter"].connect(() => { + if (client.has_adapter) show_all(); + else hide(); + }); + // Create our popover popover = new Budgie.Popover(ebox); popover.set_size_request(275, -1); @@ -65,7 +112,7 @@ public class BluetoothIndicator : Bin { bluetooth_switch = new Switch() { tooltip_text = _("Turn Bluetooth on or off"), }; - bluetooth_switch.notify["active"].connect(on_switch_activate); + switch_handler_id = bluetooth_switch.notify["active"].connect(on_switch_activate); header.pack_start(switch_label); header.pack_end(bluetooth_switch, false, false); @@ -93,12 +140,25 @@ public class BluetoothIndicator : Bin { var placeholder = new Box(Orientation.VERTICAL, 18) { margin_top = 18, }; - var placeholder_label = new Label(_("No paired Bluetooth devices found.\n\nVisit Bluetooth settings to pair a device.")) { + + var label_attributes = new Pango.AttrList(); + var weight_attr = new Pango.FontDescription(); + weight_attr.set_weight(Pango.Weight.BOLD); + label_attributes.insert(new Pango.AttrFontDesc(weight_attr)); + + placeholder_label = new Label(null) { + attributes = label_attributes, justify = CENTER, }; + placeholder_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); placeholder_label.get_style_context().add_class("bluetooth-placeholder"); + placeholder_sublabel = new Label(null) { + justify = CENTER, + wrap = true, + }; + var placeholder_button = new Button.with_label(_("Open Bluetooth Settings")) { relief = HALF, }; @@ -106,55 +166,14 @@ public class BluetoothIndicator : Bin { placeholder_button.clicked.connect(on_settings_activate); placeholder.pack_start(placeholder_label, false); + placeholder.pack_start(placeholder_sublabel, false); placeholder.pack_start(placeholder_button, false); placeholder.show_all(); // Without this, it never shows. Because... reasons? devices_box.set_placeholder(placeholder); scrolled_window.add(devices_box); - // Create our Bluetooth client - client = new BluetoothClient(); - - client.device_added.connect((device) => { - // Remove any existing rows for this device - remove_device(device); - // Add the new device to correctly update its status - add_device(device); - }); - - client.device_removed.connect((device) => { - remove_device(device); - }); - - // Handle when a UPower device has been added - client.upower_device_added.connect((up_device) => { - devices_box.foreach((row) => { - var device_row = row as BTDeviceRow; - if (device_row.device.address == up_device.serial) { - device_row.up_device = up_device; - } - }); - }); - - // Handle when a UPower device has been removed - client.upower_device_removed.connect((path) => { - devices_box.foreach((row) => { - var device_row = row as BTDeviceRow; - if (((DBusProxy) device_row.device).get_object_path() == path) { - device_row.up_device = null; - } - }); - }); - - client.global_state_changed.connect(on_client_state_changed); - - // Show or hide the panel widget if we have a Bluetooth adapter or not - client.notify["has-adapter"].connect(() => { - if (client.has_adapter) show_all(); - else hide(); - }); - // Make sure our starting icon is correct - update_tray_icon(); + update_state_ui(); add(ebox); box.pack_start(header); @@ -171,17 +190,12 @@ public class BluetoothIndicator : Bin { if (e.button != BUTTON_MIDDLE) return EVENT_PROPAGATE; // Disconnect all Bluetooth on middle click - client.set_all_powered.begin(!client.get_powered(), (obj, res) => { - client.check_powered(); - }); + var enabled = client.airplane_mode_enabled(); + client.set_airplane_mode(!enabled); return Gdk.EVENT_STOP; } - private void on_client_state_changed(bool enabled, bool connected) { - bluetooth_switch.active = enabled; - } - private void on_settings_activate() { this.popover.hide(); @@ -197,10 +211,8 @@ public class BluetoothIndicator : Bin { private void on_switch_activate() { // Turn Bluetooth on or off - client.set_all_powered.begin(bluetooth_switch.active, (obj, res) => { - client.check_powered(); - update_tray_icon(); - }); + var active = bluetooth_switch.active; + client.set_airplane_mode(!active); // If the switch is active, then Bluetooth is enabled. So invert the value } private void add_device(Device1 device) { @@ -209,7 +221,6 @@ public class BluetoothIndicator : Bin { var widget = new BTDeviceRow(device); widget.properties_updated.connect(() => { - client.check_powered(); devices_box.invalidate_filter(); devices_box.invalidate_sort(); }); @@ -252,18 +263,36 @@ public class BluetoothIndicator : Bin { * Filters out any unpaired devices from our listbox. */ private bool filter_paired_devices(ListBoxRow row) { + if (client.airplane_mode_enabled()) return false; + return ((BTDeviceRow) row).device.paired || ((BTDeviceRow) row).device.connected; } /** - * Update the tray icon used depending on the current Bluetooth state. + * Update the tray icon and Bluetooth switch state to reflect the current + * state of airplane mode. */ - private void update_tray_icon() { - if (bluetooth_switch.active) { - image.set_from_icon_name("bluetooth-active", IconSize.MENU); - } else { + private void update_state_ui() { + var enabled = client.airplane_mode_enabled(); + + // Update the tray icon and placeholder text + if (enabled) { // Airplane mode is on, so Bluetooth is disabled image.set_from_icon_name("bluetooth-disabled", IconSize.MENU); + placeholder_label.label = (_("Airplane mode is on.")); + placeholder_sublabel.label = _("Bluetooth is disabled while airplane mode is on."); + } else { // Airplane mode is off, so Bluetooth is enabled + image.set_from_icon_name("bluetooth-active", IconSize.MENU); + placeholder_label.label = _("No paired Bluetooth devices found."); + placeholder_sublabel.label = _("Visit Bluetooth settings to pair a device."); } + + // Update our switch state + SignalHandler.block(bluetooth_switch, switch_handler_id); + bluetooth_switch.active = !enabled; // Airplane mode value is opposite of our switch state + SignalHandler.unblock(bluetooth_switch, switch_handler_id); + + devices_box.invalidate_filter(); + devices_box.invalidate_sort(); } } From b175a064d01d77c0246b26f1cd20be53087b3bc1 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 15 Mar 2023 20:24:12 -0400 Subject: [PATCH 53/81] bluetooth-indicator: Use label attributes to achieve consistent styling across all themes Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 19 ++++++++++++++++--- src/theme/common/_bluetooth.scss | 11 ----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 1084007e4..55d96dd7a 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -94,7 +94,13 @@ public class BluetoothIndicator : Bin { header.get_style_context().add_class("bluetooth-header"); // Header label + var header_attributes = new Pango.AttrList(); + var weight_attr = new Pango.FontDescription(); + weight_attr.set_weight(Pango.Weight.BOLD); + header_attributes.insert(new Pango.AttrFontDesc(weight_attr)); + var switch_label = new Label(_("Bluetooth")) { + attributes = header_attributes, halign = START, margin_start = 4, }; @@ -142,8 +148,6 @@ public class BluetoothIndicator : Bin { }; var label_attributes = new Pango.AttrList(); - var weight_attr = new Pango.FontDescription(); - weight_attr.set_weight(Pango.Weight.BOLD); label_attributes.insert(new Pango.AttrFontDesc(weight_attr)); placeholder_label = new Label(null) { @@ -278,7 +282,7 @@ public class BluetoothIndicator : Bin { // Update the tray icon and placeholder text if (enabled) { // Airplane mode is on, so Bluetooth is disabled image.set_from_icon_name("bluetooth-disabled", IconSize.MENU); - placeholder_label.label = (_("Airplane mode is on.")); + placeholder_label.label = _("Airplane mode is on."); placeholder_sublabel.label = _("Bluetooth is disabled while airplane mode is on."); } else { // Airplane mode is off, so Bluetooth is enabled image.set_from_icon_name("bluetooth-active", IconSize.MENU); @@ -384,7 +388,15 @@ public class BTDeviceRow : ListBoxRow { var battery_box = new Box(Orientation.HORIZONTAL, 0); battery_icon = new Image(); + + var label_attributes = new Pango.AttrList(); + var desc = new Pango.FontDescription(); + desc.set_stretch(Pango.Stretch.ULTRA_CONDENSED); + desc.set_weight(Pango.Weight.SEMILIGHT); + label_attributes.insert(new Pango.AttrFontDesc(desc)); + battery_label = new Label(null) { + attributes = label_attributes, halign = START, }; battery_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); @@ -408,6 +420,7 @@ public class BTDeviceRow : ListBoxRow { spinner = new Spinner(); status_label = new Label(null) { + attributes = label_attributes, halign = START, }; status_label.get_style_context().add_class("bluetooth-device-status"); diff --git a/src/theme/common/_bluetooth.scss b/src/theme/common/_bluetooth.scss index 366eb946e..9c2d67536 100644 --- a/src/theme/common/_bluetooth.scss +++ b/src/theme/common/_bluetooth.scss @@ -1,17 +1,6 @@ .bluetooth-popover { - .bluetooth-header { - > label { - font-weight: bold; - } - } - .bluetooth-device-row { padding-top: 2px; padding-bottom: 2px; - - .bluetooth-battery-label, - .bluetooth-device-status { - font-size: small; - } } } From 4b08e87f6c3e71df4b243b7a8dde1f64424f0991 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Thu, 16 Mar 2023 15:50:33 -0400 Subject: [PATCH 54/81] build: Reintroduce option to compile without Bluetooth Signed-off-by: Evan Maddock --- meson.build | 8 ++++++++ meson_options.txt | 8 -------- src/panel/applets/status/StatusApplet.vala | 6 ++++++ src/panel/applets/status/meson.build | 15 ++++++++++----- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/meson.build b/meson.build index 3a6dae047..e6ffac85f 100644 --- a/meson.build +++ b/meson.build @@ -156,6 +156,13 @@ if xdg_appdir == '' endif endif +# Bluetooth option. BSD systems have no Bluetooth stack, so this allows +# BSD systems to compile and run Budgie. +with_bluetooth = get_option('with-bluetooth') +if with_bluetooth == true + add_project_arguments('-D', 'WITH_BLUETOOTH', language: 'vala') +endif + # GVC rpath. it's evil, but gvc will bomb out glib2 due to static linking weirdness now, # so we have to use a shared library to prevent multiple registration of the same types.. rpath_libdir = join_paths(libdir, meson.project_name()) @@ -216,6 +223,7 @@ report = [ '', ' gtk-doc: @0@'.format(with_gtk_doc), ' stateless: @0@'.format(with_stateless), + ' bluetooth: @0@'.format(with_bluetooth), ] diff --git a/meson_options.txt b/meson_options.txt index 1206b04fd..a1c06ac76 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,15 +1,7 @@ -<<<<<<< HEAD option('use-old-zenity', type: 'boolean', value: false, description: 'Use old zenity CLI API for out-of-process dialog handling') option('with-bluetooth', type: 'boolean', value: true, description: 'Enable Bluetooth (Vala option)') option('with-gnome-screensaver', type: 'boolean', value: false, description: 'Build using gnome-screensaver as a dependency') option('with-gtk-doc', type: 'boolean', value: true, description: 'Build gtk-doc documentation') -||||||| parent of b5dbd3bf (Remove the Meson option for Bluetooth) -option('with-stateless', type: 'boolean', value: false, description: 'Enable stateless XDG paths') -option('with-bluetooth', type: 'boolean', value: true, description: 'Enable Bluetooth (Vala option)') -======= -option('with-stateless', type: 'boolean', value: false, description: 'Enable stateless XDG paths') -option('with-bluetooth', type: 'boolean', value: true, description: 'Enable Bluetooth (Vala option)', deprecated: true) ->>>>>>> b5dbd3bf (Remove the Meson option for Bluetooth) option('with-hibernate', type: 'boolean', value: true, description: 'Include support for system hibernation') option('with-libuuid-time-safe', type: 'boolean', value: true, description: 'Enable use of LIBUUID.generate_time_safe (Vala option)') option('with-polkit', type: 'boolean', value: true, description: 'Enable PolKit support') diff --git a/src/panel/applets/status/StatusApplet.vala b/src/panel/applets/status/StatusApplet.vala index 75e5f4f08..606dafbd7 100644 --- a/src/panel/applets/status/StatusApplet.vala +++ b/src/panel/applets/status/StatusApplet.vala @@ -40,7 +40,9 @@ public class StatusSettings : Gtk.Grid { public class StatusApplet : Budgie.Applet { public string uuid { public set; public get; } protected Gtk.Box widget; +#if WITH_BLUETOOTH protected BluetoothIndicator blue; +#endif protected SoundIndicator sound; protected PowerIndicator power; protected Gtk.EventBox? wrap; @@ -104,10 +106,12 @@ public class StatusApplet : Budgie.Applet { this.setup_popover(power.ebox, power.popover); this.setup_popover(sound.ebox, sound.popover); +#if WITH_BLUETOOTH blue = new BluetoothIndicator(); widget.pack_start(blue, false, false, 0); /* Bluetooth widget shows itself - we dont control that */ this.setup_popover(blue.ebox, blue.popover); +#endif } public override void panel_position_changed(Budgie.PanelPosition position) { @@ -123,7 +127,9 @@ public class StatusApplet : Budgie.Applet { this.manager = manager; manager.register_popover(power.ebox, power.popover); manager.register_popover(sound.ebox, sound.popover); +#if WITH_BLUETOOTH manager.register_popover(blue.ebox, blue.popover); +#endif } public override bool supports_settings() { diff --git a/src/panel/applets/status/meson.build b/src/panel/applets/status/meson.build index f51686a2c..403217182 100644 --- a/src/panel/applets/status/meson.build +++ b/src/panel/applets/status/meson.build @@ -19,17 +19,22 @@ applet_status_resources = gnome.compile_resources( ) applet_status_sources = [ - 'BluetoothClient.vala', - 'BluetoothDBus.vala', - 'BluetoothEnums.vala', - 'BluetoothIndicator.vala', - 'BluetoothObexManager.vala', 'StatusApplet.vala', 'PowerIndicator.vala', 'SoundIndicator.vala', applet_status_resources ] +if with_bluetooth == true + applet_status_sources += [ + 'BluetoothClient.vala', + 'BluetoothDBus.vala', + 'BluetoothEnums.vala', + 'BluetoothIndicator.vala', + 'BluetoothObexManager.vala' + ] +endif + applet_status_deps = [ libpanelplugin_vapi, dep_giounix, From f92036eb4d65e5494bf49ff39f60c93c34f71010 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 13 May 2023 11:10:22 -0400 Subject: [PATCH 55/81] WIP - sendto implementation Signed-off-by: Evan Maddock --- src/dialogs/meson.build | 4 ++ src/dialogs/sendto/Application.vala | 65 +++++++++++++++++++++++++++++ src/dialogs/sendto/meson.build | 24 +++++++++++ 3 files changed, 93 insertions(+) create mode 100644 src/dialogs/sendto/Application.vala create mode 100644 src/dialogs/sendto/meson.build diff --git a/src/dialogs/meson.build b/src/dialogs/meson.build index e11671896..3262d7e30 100644 --- a/src/dialogs/meson.build +++ b/src/dialogs/meson.build @@ -2,5 +2,9 @@ if with_polkit == true subdir('polkit') endif +if with_bluetooth == true + subdir('sendto') +endif + subdir('power') subdir('run') diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala new file mode 100644 index 000000000..23373ea86 --- /dev/null +++ b/src/dialogs/sendto/Application.vala @@ -0,0 +1,65 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +public class SendtoApp : Gtk.Application { + public const OptionEntry[] BLUETOOTH_OPTIONS = { + { "silent", 's', 0, OptionArg.NONE, out silent, "Run application in the background", null }, + { "send", 'f', 0, OptionArg.NONE, out send, "Send file to Bluetooth device", null }, + { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Get files", null }, + null + }; + + public static bool silent = false; + public static bool active_once = false; + public static bool send = false; + [CCode (array_length = false, array_null_terminated = true)] + public static string[]? arg_files = {}; + + construct { + application_id = "org.buddiesofbudgie.bluetooth-sendto-dialog"; + flags |= ApplicationFlags.HANDLES_COMMAND_LINE; + Intl.setlocale(LocaleCategory.ALL, ); + } + + public override int command_line(ApplicationCommandLine command) { + string[] args_cmd = command.get_arguments(); + unowned string[] args = args_cmd; + var context = new OptionContext(); + context.add_main_entries(BLUETOOTH_OPTIONS, null); + + try { + context.parse(ref args); + } catch (Error e) { + warning("Error parsing command args: %s", e.message); + } + + activate(); + + return 0; + } + + public override void activate() { + if (silent) { + if (active_once) { + release(); + } + hold(); + silent = false; + } + + // TODO: ObjectManager + } +} + +public static int main(string[] args) { + var app = new SendtoApp(); + return app.run(args); +} diff --git a/src/dialogs/sendto/meson.build b/src/dialogs/sendto/meson.build new file mode 100644 index 000000000..7a68bb603 --- /dev/null +++ b/src/dialogs/sendto/meson.build @@ -0,0 +1,24 @@ +sendto_sources = [ + 'Application.vala', +] + +sendto_deps = [ + dep_giounix, + dep_gtk3, + link_libconfig, + link_libtheme +] + +executable( + 'budgie-sendto-dialog', + sendto_sources, + dependencies: sendto_deps, + vala_args: [ + '--vapidir', dir_libtheme, + '--vapidir', dir_libconfig, + '--pkg', 'budgie-config', + '--pkg', 'theme' + ], + install: true, + install_dir: libexecdir, +) From e58bd4bb4e91b04158fa0d5f544a3dc00226e9f5 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 12 Aug 2023 13:01:02 -0400 Subject: [PATCH 56/81] [WIP] bluetooth: Implement bluetooth sendto functionality Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 209 ++++++++- src/dialogs/sendto/Dialog/DeviceRow.vala | 147 ++++++ src/dialogs/sendto/Dialog/FileSender.vala | 423 ++++++++++++++++++ src/dialogs/sendto/Dialog/ScanDialog.vala | 201 +++++++++ src/dialogs/sendto/Services/Adapter.vala | 31 ++ src/dialogs/sendto/Services/Device.vala | 36 ++ src/dialogs/sendto/Services/Manager.vala | 204 +++++++++ src/dialogs/sendto/Services/ObexAgent.vala | 103 +++++ src/dialogs/sendto/Services/Session.vala | 21 + src/dialogs/sendto/Services/Transfer.vala | 26 ++ src/dialogs/sendto/meson.build | 35 +- .../org.buddiesofbudgie.sendto-daemon.desktop | 12 + .../sendto/org.buddiesofbudgie.sendto.desktop | 9 + .../applets/status/BluetoothIndicator.vala | 4 +- 14 files changed, 1433 insertions(+), 28 deletions(-) create mode 100644 src/dialogs/sendto/Dialog/DeviceRow.vala create mode 100644 src/dialogs/sendto/Dialog/FileSender.vala create mode 100644 src/dialogs/sendto/Dialog/ScanDialog.vala create mode 100644 src/dialogs/sendto/Services/Adapter.vala create mode 100644 src/dialogs/sendto/Services/Device.vala create mode 100644 src/dialogs/sendto/Services/Manager.vala create mode 100644 src/dialogs/sendto/Services/ObexAgent.vala create mode 100644 src/dialogs/sendto/Services/Session.vala create mode 100644 src/dialogs/sendto/Services/Transfer.vala create mode 100644 src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop create mode 100644 src/dialogs/sendto/org.buddiesofbudgie.sendto.desktop diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index 23373ea86..cc5eeae04 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -9,44 +9,130 @@ * (at your option) any later version. */ -public class SendtoApp : Gtk.Application { - public const OptionEntry[] BLUETOOTH_OPTIONS = { - { "silent", 's', 0, OptionArg.NONE, out silent, "Run application in the background", null }, - { "send", 'f', 0, OptionArg.NONE, out send, "Send file to Bluetooth device", null }, - { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Get files", null }, - null +public class SendtoApplication : Gtk.Application { + public const OptionEntry[] OPTIONS = { + { "silent", 's', 0, OptionArg.NONE, out silent, "Run the application in the background", null }, + { "send", 'f', 0, OptionArg.NONE, out send, "Send a file via Bluetooth", null }, + { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Get files", null }, // TODO: Better description + { null }, }; public static bool silent = false; - public static bool active_once = false; - public static bool send = false; + public static bool send = true; + public static bool active_once; [CCode (array_length = false, array_null_terminated = true)] public static string[]? arg_files = {}; + private Bluetooth.ObjectManager manager; + private Bluetooth.Obex.Agent agent; + private Bluetooth.Obex.Transfer transfer; + + private FileSender file_sender; + private List file_senders; + private ScanDialog scan_dialog; + construct { - application_id = "org.buddiesofbudgie.bluetooth-sendto-dialog"; + application_id = "org.buddiesofbudgie.Sendto"; flags |= ApplicationFlags.HANDLES_COMMAND_LINE; - Intl.setlocale(LocaleCategory.ALL, ); } public override int command_line(ApplicationCommandLine command) { - string[] args_cmd = command.get_arguments(); - unowned string[] args = args_cmd; - var context = new OptionContext(); - context.add_main_entries(BLUETOOTH_OPTIONS, null); + var command_args = command.get_arguments(); + unowned var args = command_args; + var context = new OptionContext(null); + context.add_main_entries(OPTIONS, null); + // Try to parse the command args try { context.parse(ref args); } catch (Error e) { - warning("Error parsing command args: %s", e.message); + warning("Unable to parse command args: %s", e.message); } activate(); + // Exit early if no files to send + if (!send) return 0; + + File[] files = {}; + foreach (unowned var arg_file in arg_files) { + var file = command.create_file_for_arg(arg_file); + + if (file.query_exists()) { + files += file; + } else { + warning("File not found: %s", file.get_path()); + } + } + + // If we weren't given any files, open a file picker dialog + if (files.length == 0 && !silent) { + var picker = new Gtk.FileChooserDialog( + _("Files to send"), + null, + Gtk.FileChooserAction.OPEN, + _("_Cancel"), Gtk.ResponseType.CANCEL, + _("_Open"), Gtk.ResponseType.ACCEPT + ) { + select_multiple = true, + }; + + if (picker.run() == Gtk.ResponseType.ACCEPT) { + var picked_files = picker.get_files(); + picked_files.foreach((file) => { + files += file; + }); + } + + picker.destroy(); + } + + // Still no files, exit + if (files.length == 0) return 0; + + // Create the Bluetooth scanner dialog if it doesn't yet exist + if (scan_dialog == null) { + scan_dialog = new ScanDialog(this, manager); + + // Wait for asyncronous initialization before showing the dialog + Idle.add(() => { + scan_dialog.show_all(); + return Source.REMOVE; + }); + } else { + // Dialog already exists, present it + scan_dialog.present(); + } + + // Clear our pointer when the scan dialog is destroyed + scan_dialog.destroy.connect(() => { + scan_dialog = null; + }); + + // Send the files when a device has been selected + scan_dialog.send_file.connect((device) => { + if (!insert_sender(files, device)) { + file_sender = new FileSender(this); + file_sender.add_files(files, device); + file_senders.append(file_sender); + file_sender.show_all(); + file_sender.destroy.connect(() => { + file_senders.foreach((sender) => { + if (sender.device == file_sender.device) { + file_senders.remove_link(file_senders.find(sender)); + } + }); + }); + } + }); + + arg_files = {}; + send = false; + return 0; } - public override void activate() { + protected override void activate() { if (silent) { if (active_once) { release(); @@ -55,11 +141,98 @@ public class SendtoApp : Gtk.Application { silent = false; } - // TODO: ObjectManager + if (manager == null) { + file_senders = new List(); + + manager = new Bluetooth.ObjectManager(); + manager.notify["has-object"].connect(() => { + var build_path = Path.build_filename(Environment.get_home_dir(), ".local", "share", "contractor"); + var file = File.new_for_path( + Path.build_filename( + build_path, + Environment.get_application_name() + ".contract" + ) + ); + var file_exists = file.query_exists(); + + // Create the parent directory for the contract file if it doesn't exist + if (!File.new_for_path(build_path).query_exists()) { + DirUtils.create(build_path, 0700); + } + + // If we have Bluetooth devices, create our Obex Agent and contract file + if (manager.has_object) { + // Create our Obex Agent if we haven't been activated yet + if (!active_once) { + agent = new Bluetooth.Obex.Agent(); + agent.transfer_view.connect(dialog_active); + // TODO: Connect agent signals + active_once = true; + } + + // Create and write to our Obex contract file if it doesn't exist + if (!file_exists) { + var keyfile = new KeyFile(); + keyfile.set_string ("Contractor Entry", "Name", _("Send Files via Bluetooth")); + keyfile.set_string ("Contractor Entry", "Icon", "bluetooth-active"); + keyfile.set_string ("Contractor Entry", "Description", _("Send files to device…")); + keyfile.set_string ("Contractor Entry", "Exec", "org.buddiesofbudgie.sendto -f %F"); + keyfile.set_string ("Contractor Entry", "MimeType", "!inode;"); + + try { + keyfile.save_to_file(file.get_path()); + } catch (Error e) { + critical("Error saving contract file: %s", e.message); + } + } + } else { + // Delete the contract file if it exists + if (file_exists) { + try { + file.delete(); + } catch (Error e) { + critical("Error deleting old contract file: %s", e.message); + } + } + } + }); + } + } + + private void dialog_active(string session_path) { + // TODO: Receivers + + // Show any file sender dialogs if there is a transfer session for the + // given path + file_senders.foreach((sender) => { + if (sender.transfer.session == session_path) { + sender.show_all(); + } + }); + } + + private bool insert_sender(File[] files, Bluetooth.Device device) { + bool exists = false; + + // Pass the files to send to the correct sender + file_senders.foreach((sender) => { + if (sender.device == device) { + sender.add_files(files, device); + sender.present(); + exists = true; + } + }); + + return exists; } } public static int main(string[] args) { - var app = new SendtoApp(); + Intl.setlocale(LocaleCategory.ALL, ""); + Intl.bindtextdomain(Budgie.GETTEXT_PACKAGE, Budgie.LOCALEDIR); + Intl.bind_textdomain_codeset(Budgie.GETTEXT_PACKAGE, "UTF-8"); + Intl.textdomain(Budgie.GETTEXT_PACKAGE); + + var app = new SendtoApplication(); return app.run(args); } diff --git a/src/dialogs/sendto/Dialog/DeviceRow.vala b/src/dialogs/sendto/Dialog/DeviceRow.vala new file mode 100644 index 000000000..1fbb9d3bd --- /dev/null +++ b/src/dialogs/sendto/Dialog/DeviceRow.vala @@ -0,0 +1,147 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +public class DeviceRow : Gtk.ListBoxRow { + public unowned Bluetooth.Adapter adapter { get; construct; } + public Bluetooth.Device device { get; construct; } + + private static Gtk.SizeGroup size_group; + + private Gtk.Button send_button; + private Gtk.Image state_image; + private Gtk.Label state_label; + + public signal void send_clicked(Bluetooth.Device device); + + public DeviceRow(Bluetooth.Device device, Bluetooth.Adapter adapter) { + Object(device: device, adapter: adapter); + } + + static construct { + size_group = new Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL); + } + + construct { + var image = new Gtk.Image.from_icon_name(device.icon ?? "bluetooth-active", Gtk.IconSize.DND); + + state_image = new Gtk.Image.from_icon_name("user-offline", Gtk.IconSize.MENU) { + valign = Gtk.Align.END, + halign = Gtk.Align.END, + }; + + state_label = new Gtk.Label(null) { + use_markup = true, + xalign = 0, + }; + + var overlay = new Gtk.Overlay(); + overlay.tooltip_text = device.address; + overlay.add(image); + overlay.add_overlay(state_image); + + string? device_name = device.alias; + if (device_name == null) { + if (device.icon == null) { + device_name = get_name_from_icon(); + } else { + device_name = device.address; + } + } + + var label = new Gtk.Label(device_name) { + ellipsize = Pango.EllipsizeMode.END, + hexpand = true, + xalign = 0, + }; + + send_button = new Gtk.Button() { + valign = Gtk.Align.CENTER, + label = _("Send"), + }; + + size_group.add_widget(send_button); + + var grid = new Gtk.Grid() { + margin = 6, + column_spacing = 6, + orientation = Gtk.Orientation.HORIZONTAL, + }; + + grid.attach(overlay, 0, 0, 1, 2); + grid.attach(label, 1, 0, 1, 1); + grid.attach(state_label, 1, 1, 1, 1); + grid.attach(send_button, 4, 0, 1, 2); + + add(grid); + + show_all(); + + set_sensitive(adapter.powered); + set_status(device.connected); + + ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { + var powered = changed.lookup_value("Powered", new VariantType("b")); + if (powered != null) { + set_sensitive(adapter.powered); + } + }); + + ((DBusProxy) device).g_properties_changed.connect((changed, invalid) => { + var connected = changed.lookup_value("Connected", new VariantType("b")); + if (connected != null) { + set_status(device.connected); + } + + var name = changed.lookup_value("Name", new VariantType("s")); + if (name != null) { + label.label = device.alias; + } + + var icon = changed.lookup_value("Icon", new VariantType("s")); + if (icon != null) { + image.icon_name = device.icon ?? "bluetooth-active"; + } + }); + + state_label.label = Markup.printf_escaped("%s", get_name_from_icon()); + + // Connect the send button + send_button.clicked.connect(() => { + send_clicked(device); + get_toplevel().destroy(); + }); + } + + private string get_name_from_icon() { + switch (device.icon) { + case "audio-card": + return _("Speaker"); + case "input-gaming": + return _("Controller"); + case "input-keyboard": + return _("Keyboard"); + case "input-mouse": + return _("Mouse"); + case "input-tablet": + return _("Tablet"); + case "input-touchpad": + return _("Touchpad"); + case "phone": + return _("Phone"); + default: + return device.address; + } + } + + private void set_status(bool status) { + state_image.icon_name = status ? "user-available" : "user-offline"; + } +} diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala new file mode 100644 index 000000000..08f2df0ee --- /dev/null +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -0,0 +1,423 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +public class FileSender : Gtk.Dialog { + public Bluetooth.Obex.Transfer transfer; + public Bluetooth.Device device; + + private int current_file = 0; + private int total_files = 0; + private uint64 total_size = 0; + private int start_time = 0; + + private DBusConnection connection; + private DBusProxy client_proxy; + private DBusProxy session; + private File file_path; + private ObjectPath session_path; + + private Gtk.ListStore file_store; + + private Gtk.Label path_label; + private Gtk.Label device_label; + private Gtk.Label filename_label; + private Gtk.Label rate_label; + private Gtk.Label progress_label; + private Gtk.ProgressBar progress_bar; + private Gtk.Image icon_label; + + public FileSender(Gtk.Application application) { + Object(application: application, resizable: false); + } + + construct { + file_store = new Gtk.ListStore(1, typeof(GLib.File)); + + var icon_image = new Gtk.Image.from_icon_name ("bluetooth-active", Gtk.IconSize.DIALOG) { + valign = Gtk.Align.END, + halign = Gtk.Align.END, + }; + + icon_label = new Gtk.Image() { + valign = Gtk.Align.END, + halign = Gtk.Align.END, + }; + + var overlay = new Gtk.Overlay(); + overlay.add(icon_image); + overlay.add_overlay(icon_label); + + path_label = new Gtk.Label(Markup.printf_escaped("%s:", _("From"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + path_label.get_style_context().add_class("primary"); + + device_label = new Gtk.Label(Markup.printf_escaped("%s:", _("To"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + filename_label = new Gtk.Label(Markup.printf_escaped("%s:", _("File name"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + rate_label = new Gtk.Label(Markup.printf_escaped("%s:", _("Transfer rate"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + progress_bar = new Gtk.ProgressBar() { + hexpand = true, + }; + + progress_label = new Gtk.Label(null) { + max_width_chars = 45, + hexpand = false, + wrap = true, + xalign = 0, + }; + + var message_grid = new Gtk.Grid() { + column_spacing = 0, + width_request = 450, + margin_start = 10, + margin_end = 15 + }; + + message_grid.attach(overlay, 0, 0, 1, 3); + message_grid.attach(path_label, 1, 0, 1, 1); + message_grid.attach(device_label, 1, 1, 1, 1); + message_grid.attach(filename_label, 1, 2, 1, 1); + message_grid.attach(rate_label, 1, 3, 1, 1); + message_grid.attach(progress_bar, 1, 4, 1, 1); + message_grid.attach(progress_label, 1, 5, 1, 1); + + get_content_area().add(message_grid); + + // Now add the dialog buttons + add_button(_("Close"), Gtk.ResponseType.CLOSE); + var reject_transfer = add_button(_("Cancel"), Gtk.ResponseType.CANCEL); + reject_transfer.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + // Hook up the responses + response.connect((response_id) => { + if (response_id == Gtk.ResponseType.CANCEL) { + // Cancel the current transfer if it is active + if (transfer != null && transfer.status == "active") { + try { + transfer.cancel(); + } catch (Error e) { + warning("Error cancelling Bluetooth transfer: %s", e.message); + } + + // TODO: remove_session.begin(); + } + + destroy(); + } else { + // Close button clicked, hide or close + if (transfer.status == "active") { + hide_on_delete(); + } else { + destroy(); + } + } + }); + + delete_event.connect(() => { + if (transfer.status == "active") { + return hide_on_delete(); + } else { + destroy(); + } + }); + } + + public void add_files(File[] files, Bluetooth.Device device) { + // Add each file to our list of files + foreach (var file in files) { + Gtk.TreeIter iter; + file_store.append(out iter); + file_store.set(iter, 0, file); + } + + this.device = device; + + Gtk.TreeIter iter; + file_store.get_iter_first(out iter); + file_store.get(iter, 0, out file_path); + + total_n_current(); + create_session.begin(); + } + + private void total_n_current(bool total = false) { + total_files = 0; + int current = 0; + + file_store.foreach((model, path, iter) => { + File file; + model.get(iter, 0, out file); + + if (file == file_path) { + current = total_files; + } + + total_files++; + return false; + }); + + if (!total) { + current_file = current + 1; + } + } + + private async void create_session() { + try { + // Create our Obex client + connection = yield Bus.get(BusType.SESSION); + client_proxy = yield new DBusProxy( + connection, + DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, + null, + "org.bluez.obex", + "/org/bluez/obex", + "org.bluez.obex.Client1" + ); + + // Update the labels + path_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), file_path.get_parent().get_path())); + device_label.set_markup(GLib.Markup.printf_escaped(_("To: %s"), device.alias)); + icon_label.set_from_gicon(new ThemedIcon(device.icon == null ? "bluetooth-active" : device.icon), Gtk.IconSize.LARGE_TOOLBAR); + progress_label.label = _("Trying to connect to %s…").printf(device.alias); + + // Prepare to send the file + VariantBuilder builder = new VariantBuilder(VariantType.DICTIONARY); + builder.add("{sv}", "Target", new Variant.string("opp")); + Variant parameters = new Variant("(sa{sv})", device.address, builder); + Variant variant_client = yield client_proxy.call("CreateSession", parameters, GLib.DBusCallFlags.NONE, -1); + variant_client.get("(o)", out session_path); + + // Create our Obex session + session = yield new GLib.DBusProxy ( + connection, + GLib.DBusProxyFlags.NONE, + null, + "org.bluez.obex", + session_path, + "org.bluez.obex.ObjectPush1" + ); + + // Start the transfer + send_file.begin(); + } catch (Error e) { + // Hide ourselves + hide_on_delete(); + + // Create a dialog asking the user to retry the transfer + var retry_dialog = new Gtk.MessageDialog( + this, + Gtk.DialogFlags.MODAL, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.NONE, + null + ) { + text = _("Connecting to '%s' failed").printf(device.alias), + secondary_text = "%s\n%s".printf( + "Transferring file '%s' failed.".printf(file_path.get_basename()), + _("The file has not been transferred.") + ), + }; + + retry_dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL); + var suggested_button = retry_dialog.add_button(_("Accept"), Gtk.ResponseType.ACCEPT); + suggested_button.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + retry_dialog.response.connect((response_id) => { + if (response_id == Gtk.ResponseType.ACCEPT) { + create_session.begin(); + present(); + } else { + destroy(); + } + + retry_dialog.destroy(); + }); + + retry_dialog.show_all(); + progress_label.label = e.message.split("org.bluez.obex.Error.Failed:")[1]; + warning("Error transferring '%s' to '%s': %s", file_path.get_basename(), device.alias, e.message); + } + } + + private async void remove_session() { + try { + yield client_proxy.call("RemoveSession", new Variant("(o)", session_path), DBusCallFlags.NONE, -1); + } catch (Error e) { + warning("Error removing Obex transfer session: %s", e.message); + } + } + + private async void send_file() { + // Update the labels + path_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), file_path.get_parent().get_path())); + device_label.set_markup(GLib.Markup.printf_escaped(_("To: %s"), device.alias)); + icon_label.set_from_gicon(new ThemedIcon(device.icon == null ? "bluetooth-active" : device.icon), Gtk.IconSize.LARGE_TOOLBAR); + progress_label.label = _("Waiting for acceptance on %s…").printf(device.alias); + + try { + var variant = yield session.call("SendFile", new Variant("(s)", file_path.get_path()), DBusCallFlags.NONE, -1); + start_time = (int) get_real_time(); + + ObjectPath object_path; + variant.get("(oa{sv})", out object_path, null); + + transfer = Bus.get_proxy_sync( + BusType.SESSION, + "org.bluez.obex", + object_path, + DBusProxyFlags.NONE + ); + + filename_label.set_markup(Markup.printf_escaped("File name: %s", transfer.name)); + total_size = transfer.size; + + ((DBusProxy) transfer).g_properties_changed.connect((changed, invalid) => { + update_progress(); + }); + } catch (Error e) { + warning("Error transferring file '%s' to '%s': %s", transfer.name, device.alias, e.message); + } + } + + private void update_progress() { + switch (transfer.status) { + case "error": + hide_on_delete(); + + // Create a dialog asking the user to retry the transfer + var retry_dialog = new Gtk.MessageDialog( + this, + Gtk.DialogFlags.MODAL, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.NONE, + null + ) { + text = _("Transferring '%s' failed").printf(file_path.get_basename()), + secondary_text = "%s\n%s".printf( + _("The transfer was interrupted or declined by %s.").printf(device.alias), + _("The file has not been transferred.") + ), + }; + + retry_dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL); + var suggested_button = retry_dialog.add_button(_("Accept"), Gtk.ResponseType.ACCEPT); + suggested_button.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + retry_dialog.response.connect((response_id) => { + if (response_id == Gtk.ResponseType.ACCEPT) { + create_session.begin(); + present(); + } else { + destroy(); + } + + retry_dialog.destroy(); + }); + + retry_dialog.show_all(); + progress_bar.fraction = 0.0; + remove_session.begin(); + break; + case "active": + on_transfer_progress(transfer.transferred); + break; + case "complete": + send_notify(); + + if (!try_next_file()) { + remove_session.begin(); + destroy(); + } + break; + default: + break; + } + } + + private void on_transfer_progress(uint64 transferred) { + progress_bar.fraction = (double) transferred / (double) total_size; + int current_time = (int) get_real_time(); + int elapsed_time = (current_time - start_time) / 1000000; + if (current_time < start_time + 1000000) return; + if (elapsed_time == 0) return; + + uint64 transfer_rate = transferred / elapsed_time; + if (transfer_rate == 0) return; + + rate_label.label = Markup.printf_escaped (_("Transfer rate: %s"), format_size(transfer_rate)); + uint64 remaining_time = (total_size - transferred) / transfer_rate; + progress_label.label = _("(%i/%i) %s of %s sent. Time remaining: %s").printf (current_file, total_files, format_size(transferred), format_size(total_size), format_time((int) remaining_time)); + } + + private string format_time(int seconds) { + if (seconds < 0) seconds = 0; + if (seconds < 60) return ngettext("%d second", "%d seconds", seconds).printf(seconds); + + int minutes; + if (seconds < 60 * 60) { + minutes = (seconds + 30) / 60; + return ngettext("%d minute", "%d minutes", minutes).printf(minutes); + } + + int hours = seconds / (60 * 60); + if (seconds < 60 * 60 * 4) { + minutes = (seconds - hours * 60 * 60 + 30) / 60; + string h = ngettext("%u hour", "%u hours", hours).printf(hours); + string m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + ///TRANSLATORS: For example "1 hour, 8 minutes". + return _("%s, %s").printf(h, m); + } + + return ngettext("about %d hour", "about %d hours", hours).printf(hours); + } + + private bool try_next_file() { + Gtk.TreeIter iter; + if (file_store.get_iter_from_string(out iter, current_file.to_string())) { + file_store.get(iter, 0, out file_path); + send_file.begin(); + total_n_current(); + return true; + } + + return false; + } + + private void send_notify() { + var notification = new Notification("Bluetooth"); + notification.set_icon(new ThemedIcon(device.icon)); + notification.set_title(_("File transferred successfully")); + notification.set_body(Markup.printf_escaped("From: %s Sent to: %s", file_path.get_path(), device.alias)); + notification.set_priority(NotificationPriority.NORMAL); + ((Gtk.Window) get_toplevel()).application.send_notification("org.buddiesofbudgie.bluetooth", notification); + } +} diff --git a/src/dialogs/sendto/Dialog/ScanDialog.vala b/src/dialogs/sendto/Dialog/ScanDialog.vala new file mode 100644 index 000000000..77525dc25 --- /dev/null +++ b/src/dialogs/sendto/Dialog/ScanDialog.vala @@ -0,0 +1,201 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +public class ScanDialog : Gtk.Dialog { + public Bluetooth.ObjectManager manager { get; construct; } + + private Gtk.ListBox devices_box; + + public signal void send_file(Bluetooth.Device device); + + public ScanDialog(Gtk.Application application, Bluetooth.ObjectManager manager) { + Object(application: application, manager: manager, resizable: false); + } + + construct { + var icon_image = new Gtk.Image.from_icon_name("bluetooth-active", Gtk.IconSize.DIALOG) { + valign = Gtk.Align.CENTER, + halign = Gtk.Align.CENTER, + }; + + var title_label = new Gtk.Label(_("Bluetooth File Transfer")) { + max_width_chars = 45, + use_markup = true, + wrap = true, + xalign = 0, + }; + title_label.get_style_context().add_class("primary"); + + var info_label = new Gtk.Label(_("Select a Bluetooth device to send files to")) { + max_width_chars = 45, + use_markup = true, + wrap = true, + xalign = 0, + }; + + var placeholder = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + var placeholder_title = new Gtk.Label(_("No devices found")) { + use_markup = true, + }; + placeholder_title.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + var placeholder_text = new Gtk.Label(_("Ensure that your devices are visable and ready for pairing")); + placeholder_text.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + + placeholder.pack_start(placeholder_title); + placeholder.pack_start(placeholder_text); + placeholder.show_all(); + + devices_box = new Gtk.ListBox() { + activate_on_single_click = true, + selection_mode = Gtk.SelectionMode.BROWSE, + }; + + devices_box.set_header_func((Gtk.ListBoxUpdateHeaderFunc) title_rows); + devices_box.set_sort_func((Gtk.ListBoxSortFunc) compare_rows); + devices_box.set_placeholder(placeholder); + + var scrolled_window = new Gtk.ScrolledWindow(null, null) { + expand = true, + }; + scrolled_window.add(devices_box); + + var grid = new Gtk.Grid() { + margin_bottom = 10, + }; + + grid.attach(icon_image, 0, 0, 1, 2); + grid.attach(title_label, 1, 0, 1, 1); + grid.attach(info_label, 1, 1, 1, 1); + + var devices_grid = new Gtk.Grid() { + orientation = Gtk.Orientation.VERTICAL, + valign = Gtk.Align.CENTER, + margin_left = 10, + margin_right = 10, + width_request = 350, + height_request = 350, + }; + + // devices_grid.add(frame); + devices_grid.add(scrolled_window); + + get_content_area().add(devices_grid); + + add_button(_("Close"), Gtk.ResponseType.CLOSE); + response.connect((response_id) => { + manager.stop_discovery.begin(); + destroy(); + }); + + // Connect manager signals + manager.device_added.connect(add_device); + manager.device_removed.connect(device_removed); + manager.status_discovering.connect(() => { + // TODO: dunno how or if to show this yet + }); + } + + public override void show() { + base.show(); + var devices = manager.get_devices(); + + foreach (var device in devices) { + add_device(device); + } + + manager.start_discovery.begin(); + } + + private void add_device(Bluetooth.Device device) { + bool exists = false; + + // Check if this device has already been added + foreach (var row in devices_box.get_children()) { + if (((DeviceRow) row).device == device) { + exists = true; + break; + } + } + + if (exists) return; + + var row = new DeviceRow(device, manager.get_adapter_from_path(device.adapter)); + devices_box.add(row); + + if (devices_box.get_selected_row() == null) { + devices_box.select_row(row); + devices_box.row_activated(row); + } + + row.send_clicked.connect((device) => { + manager.stop_discovery.begin(); + send_file(device); + }); + } + + private void device_removed(Bluetooth.Device device) { + foreach (var row in devices_box.get_children()) { + if (((DeviceRow) row).device == device) { + devices_box.remove(row); + break; + } + } + } + + [CCode (instance_pos = -1)] + private int compare_rows(DeviceRow row1, DeviceRow row2) { + unowned Bluetooth.Device device1 = row1.device; + unowned Bluetooth.Device device2 = row2.device; + + if (device1.paired && !device2.paired) { + return -1; + } + + if (!device1.paired && device2.paired) { + return 1; + } + + if (device1.connected && !device2.connected) { + return -1; + } + + if (!device1.connected && device2.connected) { + return 1; + } + + if (device1.name != null && device2.name == null) { + return -1; + } + + if (device1.name == null && device2.name != null) { + return 1; + } + + var name1 = device1.name ?? device1.address; + var name2 = device2.name ?? device2.address; + return name1.collate(name2); + } + + [CCode (instance_pos = -1)] + private void title_rows(DeviceRow row1, DeviceRow? row2) { + if (row2 == null) { + var label = new Gtk.Label(_("Available Devices")) { + margin = 3, + xalign = 0, + }; + + label.get_style_context().add_class(Gtk.STYLE_CLASS_TITLE); + row1.set_header(label); + } else { + row1.set_header(null); + } + } +} diff --git a/src/dialogs/sendto/Services/Adapter.vala b/src/dialogs/sendto/Services/Adapter.vala new file mode 100644 index 000000000..046f8cff0 --- /dev/null +++ b/src/dialogs/sendto/Services/Adapter.vala @@ -0,0 +1,31 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +[DBus (name = "org.bluez.Adapter1")] +public interface Bluetooth.Adapter : Object { + public abstract string[] UUIDs { owned get; } + public abstract bool discoverable { get; set; } + public abstract bool discovering { get; } + public abstract bool pairable { get; set; } + public abstract bool powered { get; set; } + public abstract string address { owned get; } + public abstract string alias { owned get; set; } + public abstract string modalias { owned get; } + public abstract string name { owned get; } + public abstract uint @class { get; } + public abstract uint discoverable_timeout { get; } + public abstract uint pairable_timeout { get; } + + public abstract void remove_device(ObjectPath device) throws Error; + public abstract void set_discovery_filter(HashTable properties) throws Error; + public abstract async void start_discovery() throws Error; + public abstract async void stop_discovery() throws Error; +} diff --git a/src/dialogs/sendto/Services/Device.vala b/src/dialogs/sendto/Services/Device.vala new file mode 100644 index 000000000..0a3197291 --- /dev/null +++ b/src/dialogs/sendto/Services/Device.vala @@ -0,0 +1,36 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +[DBus (name = "org.bluez.Device1")] +public interface Bluetooth.Device : Object { + 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; } + + 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; +} diff --git a/src/dialogs/sendto/Services/Manager.vala b/src/dialogs/sendto/Services/Manager.vala new file mode 100644 index 000000000..c72d6e75c --- /dev/null +++ b/src/dialogs/sendto/Services/Manager.vala @@ -0,0 +1,204 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +public class Bluetooth.ObjectManager : Object { + public bool has_object { get; private set; default = false; } + + private GLib.DBusObjectManagerClient object_manager; + + public signal void device_added(Bluetooth.Device device); + public signal void device_removed(Bluetooth.Device device); + public signal void status_discovering(); + + construct { + create_manager.begin(); + register_obex_agentmanager(); + } + + public async void create_manager() { + try { + object_manager = yield new GLib.DBusObjectManagerClient.for_bus.begin( + BusType.SYSTEM, + GLib.DBusObjectManagerClientFlags.NONE, + "org.bluez", + "/", + object_manager_proxy_get_type, + null + ); + + object_manager.get_objects().foreach((object) => { + object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); + }); + + object_manager.interface_added.connect(on_interface_added); + + object_manager.interface_removed.connect(on_interface_removed); + + object_manager.object_added.connect((object) => { + object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); + }); + + object_manager.object_removed.connect((object) => { + object.get_interfaces().foreach((iface) => on_interface_removed(object, iface)); + }); + } catch (Error e) { + critical("Error getting Bluez object manager: %s", e.message); + } + } + + //TODO: Do not rely on this when it is possible to do it natively in Vala + [CCode (cname="bluetooth_device_proxy_get_type")] + extern static GLib.Type get_device_proxy_type(); + + [CCode (cname="bluetooth_adapter_proxy_get_type")] + extern static GLib.Type get_adapter_proxy_type(); + + private GLib.Type object_manager_proxy_get_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 void register_obex_agentmanager() { + try { + var connection = GLib.Bus.get_sync(BusType.SESSION); + connection.call.begin( + "org.bluez.obex", + "/org/bluez/obex", + "org.bluez.obex.AgentManager1", + "RegisterAgent", // TODO: Do we need to worry about unregistering? + new Variant("(o)", "/org/bluez/obex/budgie"), + null, + GLib.DBusCallFlags.NONE, + -1); + } catch (Error e) { + critical("Error registering Obex agent manager: %s", e.message); + } + } + + private void on_interface_added(GLib.DBusObject object, GLib.DBusInterface iface) { + if (iface is Bluetooth.Device) { + unowned var device = (Bluetooth.Device) iface; + device_added(device); + } else if (iface is Bluetooth.Adapter) { + unowned var adapter = (Bluetooth.Adapter) iface; + has_object = true; + ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { + var discovering = changed.lookup_value("Discovering", GLib.VariantType.BOOLEAN); + if (discovering != null) { + status_discovering(); + } + }); + } + } + + private void on_interface_removed(GLib.DBusObject object, GLib.DBusInterface iface) { + if (iface is Bluetooth.Device) { + device_removed((Bluetooth.Device) iface); + } else if (iface is Bluetooth.Adapter) { + has_object = !get_adapters().is_empty; + } + } + + public Gee.LinkedList get_adapters() requires (object_manager != null) { + var adapters = new Gee.LinkedList(); + + object_manager.get_objects().foreach((object) => { + GLib.DBusInterface? iface = object.get_interface("org.bluez.Adapter1"); + if (iface == null) return; + + adapters.add(((Bluetooth.Adapter) iface)); + }); + + return (owned) adapters; + } + + public Gee.Collection get_devices() requires (object_manager != null) { + var devices = new Gee.LinkedList(); + + object_manager.get_objects().foreach((object) => { + GLib.DBusInterface? iface = object.get_interface("org.bluez.Device1"); + if (iface == null) return; + + devices.add(((Bluetooth.Device) iface)); + }); + + return (owned) devices; + } + + public async void start_discovery() { + var adapters = get_adapters(); + + foreach (var adapter in adapters) { + try { + adapter.discoverable = true; + yield adapter.start_discovery(); + } catch (Error e) { + critical("Error starting discovery on Bluetooth adapter '%s': %s", adapter.name, e.message); + } + } + } + + public bool check_discovering() { + var adapters = get_adapters(); + + foreach (var adapter in adapters) { + return adapter.discovering; + } + + return false; + } + + public async void stop_discovery() { + var adapters = get_adapters(); + + foreach (var adapter in adapters) { + adapter.discoverable = false; + + try { + if (adapter.powered && adapter.discovering) { + yield adapter.stop_discovery(); + } + } catch (Error e) { + critical("Error stopping discovery on Bluetooth adapter '%s': %s", adapter.name, e.message); + } + } + } + + public Bluetooth.Adapter? get_adapter_from_path(string path) { + GLib.DBusObject? object = object_manager.get_object(path); + + if (object != null) { + return (Bluetooth.Adapter?) object.get_interface("org.bluez.Adapter1"); + } + + return null; + } + + public Bluetooth.Device? get_device(string address) { + var devices = get_devices(); + + foreach (var device in devices) { + if (device.address == address) { + return device; + } + } + + return null; + } +} diff --git a/src/dialogs/sendto/Services/ObexAgent.vala b/src/dialogs/sendto/Services/ObexAgent.vala new file mode 100644 index 000000000..22443081d --- /dev/null +++ b/src/dialogs/sendto/Services/ObexAgent.vala @@ -0,0 +1,103 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +[DBus (name = "org.bluez.obex.Error")] +public errordomain BluezObexError { + REJECTED, + CANCELED +} + +[DBus (name = "org.bluez.obex.Agent1")] +public class Bluetooth.Obex.Agent : GLib.Object { + /*one confirmation for many files in one session */ + private GLib.ObjectPath many_files; + + public signal void response_notify(string address, GLib.ObjectPath objectpath); + public signal void response_accepted(string address, GLib.ObjectPath objectpath); + public signal void transfer_view(string session_path); + public signal void response_canceled(); + + public Agent() { + Bus.own_name( + BusType.SESSION, + "org.bluez.obex.Agent1", + GLib.BusNameOwnerFlags.NONE, + on_name_get + ); + } + + private void on_name_get(GLib.DBusConnection conn) { + try { + conn.register_object ("/org/bluez/obex/budgie", this); + } catch (Error e) { + error (e.message); + } + } + + public void transfer_active(string session_path) throws GLib.Error { + transfer_view(session_path); + } + + public void release() throws GLib.Error {} + + public async string authorize_push(GLib.ObjectPath objectpath) throws Error { + SourceFunc callback = authorize_push.callback; + BluezObexError? obex_error = null; + Bluetooth.Obex.Transfer transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", objectpath); + + if (transfer.name == null) { + throw new BluezObexError.REJECTED("Authorize Reject"); + } + + Bluetooth.Obex.Session session = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", transfer.session); + var accept_action = new SimpleAction("btaccept", VariantType.STRING); + GLib.Application.get_default().add_action(accept_action); + accept_action.activate.connect((parameter) => { + response_accepted(session.destination, objectpath); + if (callback != null) { + Idle.add((owned) callback); + } + }); + + var cancel_action = new SimpleAction("btcancel", VariantType.STRING); + GLib.Application.get_default().add_action(cancel_action); + cancel_action.activate.connect((parameter) => { + obex_error = new BluezObexError.CANCELED("Authorize Cancel"); + response_canceled(); + if (callback != null) { + Idle.add((owned) callback); + } + }); + + if (many_files == objectpath) { + Idle.add(()=>{ + response_accepted(session.destination, objectpath); + if (callback != null) { + Idle.add((owned) callback); + } + return GLib.Source.REMOVE; + }); + } else { + response_notify(session.destination, objectpath); + } + + yield; + + if (obex_error != null) throw obex_error; + + many_files = objectpath; + return transfer.name; + } + + public void cancel() throws GLib.Error { + response_canceled(); + } +} diff --git a/src/dialogs/sendto/Services/Session.vala b/src/dialogs/sendto/Services/Session.vala new file mode 100644 index 000000000..eb37057dd --- /dev/null +++ b/src/dialogs/sendto/Services/Session.vala @@ -0,0 +1,21 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +[DBus (name = "org.bluez.obex.Session1")] +public interface Bluetooth.Obex.Session : Object { + public abstract string source { owned get; } + public abstract string destination { owned get; } + public abstract uchar channel { owned get; } + public abstract string target { owned get; } + public abstract string root { owned get; } + + public abstract string get_capabilities() throws GLib.Error; +} diff --git a/src/dialogs/sendto/Services/Transfer.vala b/src/dialogs/sendto/Services/Transfer.vala new file mode 100644 index 000000000..2f01ebd1e --- /dev/null +++ b/src/dialogs/sendto/Services/Transfer.vala @@ -0,0 +1,26 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +[DBus (name = "org.bluez.obex.Transfer1")] +public interface Bluetooth.Obex.Transfer : Object { + public abstract string status { owned get; } + public abstract ObjectPath session { owned get; } + public abstract string name { owned get; } + public abstract string Type { owned get; } + public abstract uint64 time { owned get; } + public abstract uint64 size { owned get; } + public abstract uint64 transferred { owned get; } + public abstract string filename { owned get; } + + public abstract void cancel() throws GLib.Error; + public abstract void resume() throws GLib.Error; + public abstract void suspend() throws GLib.Error; +} diff --git a/src/dialogs/sendto/meson.build b/src/dialogs/sendto/meson.build index 7a68bb603..1ee04c1eb 100644 --- a/src/dialogs/sendto/meson.build +++ b/src/dialogs/sendto/meson.build @@ -1,24 +1,43 @@ sendto_sources = [ 'Application.vala', + 'Dialog/DeviceRow.vala', + 'Dialog/FileSender.vala', + 'Dialog/ScanDialog.vala', + 'Services/Adapter.vala', + 'Services/Device.vala', + 'Services/Manager.vala', + 'Services/ObexAgent.vala', + 'Services/Session.vala', + 'Services/Transfer.vala', ] sendto_deps = [ - dep_giounix, + dep_gee, + dep_glib, dep_gtk3, link_libconfig, - link_libtheme + link_libtheme, ] executable( - 'budgie-sendto-dialog', + 'org.buddiesofbudgie.sendto', sendto_sources, dependencies: sendto_deps, vala_args: [ - '--vapidir', dir_libtheme, - '--vapidir', dir_libconfig, - '--pkg', 'budgie-config', - '--pkg', 'theme' + '--vapidir', dir_libtheme, + '--vapidir', dir_libconfig, + '--pkg', 'budgie-config', + '--pkg', 'theme' ], install: true, - install_dir: libexecdir, +) + +install_data( + 'org.buddiesofbudgie.sendto-daemon.desktop', + install_dir: join_paths(confdir, 'xdg', 'autostart') +) + +install_data( + 'org.buddiesofbudgie.sendto.desktop', + install_dir: join_paths(datadir, 'applications') ) diff --git a/src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop b/src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop new file mode 100644 index 000000000..bd14ff06e --- /dev/null +++ b/src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Sendto +Comment=Bluetooth obex +Exec=org.buddiesofbudgie.sendto -s +Icon=bluetooth-active +Type=Application +NoDisplay=true +Categories=System; +X-GNOME-Autostart-Notify=false +X-GNOME-AutoRestart=true +X-GNOME-Autostart-enabled=true +OnlyShowIn=Budgie; diff --git a/src/dialogs/sendto/org.buddiesofbudgie.sendto.desktop b/src/dialogs/sendto/org.buddiesofbudgie.sendto.desktop new file mode 100644 index 000000000..90a73d39b --- /dev/null +++ b/src/dialogs/sendto/org.buddiesofbudgie.sendto.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Sendto +Comment=Bluetooth obex +Exec=org.buddiesofbudgie.sendto +Icon=bluetooth-active +Type=Application +NoDisplay=true +Categories=System; +OnlyShowIn=Budgie; diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 55d96dd7a..b7ee56aad 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -161,7 +161,7 @@ public class BluetoothIndicator : Bin { placeholder_sublabel = new Label(null) { justify = CENTER, wrap = true, - }; + }; var placeholder_button = new Button.with_label(_("Open Bluetooth Settings")) { relief = HALF, @@ -544,7 +544,7 @@ public class BTDeviceRow : ListBoxRow { private void transfer_active(string address) { if (address == device.address) update_transfer_progress(); } - + private void transfer_added(string address, Transfer transfer) { if (address == device.address) this.transfer = transfer; } From 3a20022f830ab1572a362cff3ccc07dd66946c3c Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 14 Aug 2023 14:14:51 -0400 Subject: [PATCH 57/81] sendto: Fix command line arguments Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 20 +++++++++---------- .../org.buddiesofbudgie.sendto-daemon.desktop | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index cc5eeae04..8eb71f339 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -10,18 +10,18 @@ */ public class SendtoApplication : Gtk.Application { - public const OptionEntry[] OPTIONS = { - { "silent", 's', 0, OptionArg.NONE, out silent, "Run the application in the background", null }, + private const OptionEntry[] OPTIONS = { + { "daemon", 'd', 0, OptionArg.NONE, out silent, "Run the application in the background", null }, { "send", 'f', 0, OptionArg.NONE, out send, "Send a file via Bluetooth", null }, { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Get files", null }, // TODO: Better description { null }, }; - public static bool silent = false; - public static bool send = true; - public static bool active_once; + private static bool silent = true; + private static bool send = false; + private static bool active_once; [CCode (array_length = false, array_null_terminated = true)] - public static string[]? arg_files = {}; + private static string[]? arg_files = {}; private Bluetooth.ObjectManager manager; private Bluetooth.Obex.Agent agent; @@ -37,16 +37,16 @@ public class SendtoApplication : Gtk.Application { } public override int command_line(ApplicationCommandLine command) { - var command_args = command.get_arguments(); - unowned var args = command_args; + var args = command.get_arguments(); var context = new OptionContext(null); context.add_main_entries(OPTIONS, null); // Try to parse the command args try { - context.parse(ref args); + context.parse_strv(ref args); } catch (Error e) { warning("Unable to parse command args: %s", e.message); + return 1; } activate(); @@ -66,7 +66,7 @@ public class SendtoApplication : Gtk.Application { } // If we weren't given any files, open a file picker dialog - if (files.length == 0 && !silent) { + if (files.length == 0) { var picker = new Gtk.FileChooserDialog( _("Files to send"), null, diff --git a/src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop b/src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop index bd14ff06e..bdeee97d8 100644 --- a/src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop +++ b/src/dialogs/sendto/org.buddiesofbudgie.sendto-daemon.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=Sendto Comment=Bluetooth obex -Exec=org.buddiesofbudgie.sendto -s +Exec=org.buddiesofbudgie.sendto -d Icon=bluetooth-active Type=Application NoDisplay=true From 28819c0d0472aee5e6536f9d2096d9f82c60b1a6 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 14 Aug 2023 14:18:03 -0400 Subject: [PATCH 58/81] sendto: Better description for files arg Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index 8eb71f339..514b2ac37 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -13,7 +13,7 @@ public class SendtoApplication : Gtk.Application { private const OptionEntry[] OPTIONS = { { "daemon", 'd', 0, OptionArg.NONE, out silent, "Run the application in the background", null }, { "send", 'f', 0, OptionArg.NONE, out send, "Send a file via Bluetooth", null }, - { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Get files", null }, // TODO: Better description + { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Files to send via Bluetooth", null }, // TODO: Better description { null }, }; From 6d71f8911b2a13107b56eb01e35d5c1d61454ec2 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 14 Aug 2023 14:20:35 -0400 Subject: [PATCH 59/81] sendto: Remember to remove comment Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index 514b2ac37..3060fbca2 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -13,7 +13,7 @@ public class SendtoApplication : Gtk.Application { private const OptionEntry[] OPTIONS = { { "daemon", 'd', 0, OptionArg.NONE, out silent, "Run the application in the background", null }, { "send", 'f', 0, OptionArg.NONE, out send, "Send a file via Bluetooth", null }, - { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Files to send via Bluetooth", null }, // TODO: Better description + { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Files to send via Bluetooth", null }, { null }, }; From 92de44258654c3099643a445153e4b98c0e8cc3d Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 14 Aug 2023 15:04:33 -0400 Subject: [PATCH 60/81] sendto: Properly exit if the file picker or scan dialog is closed Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index 3060fbca2..108405a6d 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -17,7 +17,7 @@ public class SendtoApplication : Gtk.Application { { null }, }; - private static bool silent = true; + private static bool silent = false; private static bool send = false; private static bool active_once; [CCode (array_length = false, array_null_terminated = true)] @@ -77,13 +77,16 @@ public class SendtoApplication : Gtk.Application { select_multiple = true, }; - if (picker.run() == Gtk.ResponseType.ACCEPT) { - var picked_files = picker.get_files(); - picked_files.foreach((file) => { - files += file; - }); + if (picker.run() != Gtk.ResponseType.ACCEPT) { + picker.destroy(); + return 0; } + var picked_files = picker.get_files(); + picked_files.foreach((file) => { + files += file; + }); + picker.destroy(); } @@ -107,6 +110,8 @@ public class SendtoApplication : Gtk.Application { // Clear our pointer when the scan dialog is destroyed scan_dialog.destroy.connect(() => { scan_dialog = null; + + if (!silent) quit(); }); // Send the files when a device has been selected From 9f73a8d7153b0f080f9b47d61415492ae24e2f52 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 14 Aug 2023 15:14:00 -0400 Subject: [PATCH 61/81] bluetooth-indicator: Hook up file send button in the header Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index b7ee56aad..b7c3a7adf 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -21,6 +21,7 @@ public class BluetoothIndicator : Bin { public Budgie.Popover? popover = null; private ListBox? devices_box = null; + private Button? send_button = null; private Switch? bluetooth_switch = null; private Label? placeholder_label = null; private Label? placeholder_sublabel = null; @@ -106,13 +107,20 @@ public class BluetoothIndicator : Bin { }; switch_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); + // Send button + send_button = new Button.from_icon_name("folder-download-symbolic", MENU) { + relief = NONE, + tooltip_text = _("Send files via Bluetooth"), + }; + send_button.clicked.connect(on_send_clicked); + // Settings button - var button = new Button.from_icon_name("preferences-system-symbolic", MENU) { + var settings_button = new Button.from_icon_name("preferences-system-symbolic", MENU) { tooltip_text = _("Bluetooth Settings") }; - button.get_style_context().add_class(STYLE_CLASS_FLAT); - button.get_style_context().remove_class(STYLE_CLASS_BUTTON); - button.clicked.connect(on_settings_activate); + settings_button.get_style_context().add_class(STYLE_CLASS_FLAT); + settings_button.get_style_context().remove_class(STYLE_CLASS_BUTTON); + settings_button.clicked.connect(on_settings_activate); // Bluetooth switch bluetooth_switch = new Switch() { @@ -122,7 +130,8 @@ public class BluetoothIndicator : Bin { header.pack_start(switch_label); header.pack_end(bluetooth_switch, false, false); - header.pack_end(button, false, false); + header.pack_end(settings_button, false, false); + header.pack_end(send_button, false, false); // Devices var scrolled_window = new ScrolledWindow(null, null) { @@ -200,6 +209,27 @@ public class BluetoothIndicator : Bin { return Gdk.EVENT_STOP; } + private void on_send_clicked() { + this.popover.hide(); + + string[] args = { "org.buddiesofbudgie.sendto", "-f" }; + var env = Environ.get(); + Pid pid; + + try { + Process.spawn_async( + null, + args, + env, + SEARCH_PATH_FROM_ENVP, + null, + out pid + ); + } catch (SpawnError e) { + warning("Error starting sendto: %s", e.message); + } + } + private void on_settings_activate() { this.popover.hide(); @@ -217,6 +247,7 @@ public class BluetoothIndicator : Bin { // Turn Bluetooth on or off var active = bluetooth_switch.active; client.set_airplane_mode(!active); // If the switch is active, then Bluetooth is enabled. So invert the value + send_button.sensitive = active; } private void add_device(Device1 device) { @@ -312,7 +343,6 @@ public class BTDeviceRow : ListBoxRow { private Revealer? revealer = null; private Spinner? spinner = null; private Label? status_label = null; - private Button? send_file_button = null; private Button? connection_button = null; private Revealer? progress_revealer = null; private Label? file_label = null; @@ -443,17 +473,6 @@ public class BTDeviceRow : ListBoxRow { toggle_connection.begin(); }); - // Send file button - send_file_button = new Button.from_icon_name("folder-download-symbolic", IconSize.BUTTON) { - relief = ReliefStyle.HALF, - tooltip_text = _("Send file…"), - }; - send_file_button.clicked.connect(() => { - - }); - send_file_button.get_style_context().add_class("circular"); - - button_box.pack_start(send_file_button, false); button_box.pack_start(connection_button, false); // Progress stuff @@ -608,12 +627,10 @@ public class BTDeviceRow : ListBoxRow { if (device.connected) { status_label.set_text(_("Connected")); connection_button.show(); - send_file_button.show(); activatable = false; } else { status_label.set_text(_("Disconnected")); connection_button.hide(); - send_file_button.hide(); activatable = true; } From 2788220cc69b193cf1705c5c631494c61891534c Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 19 Aug 2023 10:21:39 -0400 Subject: [PATCH 62/81] sendto: Implement file receiving, which manages to break everything Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 128 +++++++++- src/dialogs/sendto/Dialog/FileReceiver.vala | 253 ++++++++++++++++++++ src/dialogs/sendto/Dialog/FileSender.vala | 6 +- src/dialogs/sendto/Services/ObexAgent.vala | 2 +- src/dialogs/sendto/meson.build | 1 + 5 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 src/dialogs/sendto/Dialog/FileReceiver.vala diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index 108405a6d..af0848406 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -17,7 +17,7 @@ public class SendtoApplication : Gtk.Application { { null }, }; - private static bool silent = false; + private static bool silent = true; private static bool send = false; private static bool active_once; [CCode (array_length = false, array_null_terminated = true)] @@ -27,7 +27,9 @@ public class SendtoApplication : Gtk.Application { private Bluetooth.Obex.Agent agent; private Bluetooth.Obex.Transfer transfer; + private FileReceiver file_receiver; private FileSender file_sender; + private List file_receivers; private List file_senders; private ScanDialog scan_dialog; @@ -147,6 +149,7 @@ public class SendtoApplication : Gtk.Application { } if (manager == null) { + file_receivers = new List(); file_senders = new List(); manager = new Bluetooth.ObjectManager(); @@ -171,18 +174,19 @@ public class SendtoApplication : Gtk.Application { if (!active_once) { agent = new Bluetooth.Obex.Agent(); agent.transfer_view.connect(dialog_active); - // TODO: Connect agent signals + agent.response_accepted.connect(response_accepted); + agent.response_notify.connect(response_notify); active_once = true; } // Create and write to our Obex contract file if it doesn't exist if (!file_exists) { var keyfile = new KeyFile(); - keyfile.set_string ("Contractor Entry", "Name", _("Send Files via Bluetooth")); - keyfile.set_string ("Contractor Entry", "Icon", "bluetooth-active"); - keyfile.set_string ("Contractor Entry", "Description", _("Send files to device…")); - keyfile.set_string ("Contractor Entry", "Exec", "org.buddiesofbudgie.sendto -f %F"); - keyfile.set_string ("Contractor Entry", "MimeType", "!inode;"); + keyfile.set_string("Contractor Entry", "Name", _("Send Files via Bluetooth")); + keyfile.set_string("Contractor Entry", "Icon", "bluetooth-active"); + keyfile.set_string("Contractor Entry", "Description", _("Send files to device…")); + keyfile.set_string("Contractor Entry", "Exec", "org.buddiesofbudgie.sendto -f %F"); + keyfile.set_string("Contractor Entry", "MimeType", "!inode;"); try { keyfile.save_to_file(file.get_path()); @@ -205,7 +209,13 @@ public class SendtoApplication : Gtk.Application { } private void dialog_active(string session_path) { - // TODO: Receivers + // Show any file receiver dialogs if there is a transfer session for the + // given path + file_receivers.foreach((receiver) => { + if (receiver.transfer.session == session_path) { + receiver.show_all(); + } + }); // Show any file sender dialogs if there is a transfer session for the // given path @@ -230,6 +240,108 @@ public class SendtoApplication : Gtk.Application { return exists; } + + private void response_accepted(string address, ObjectPath path) { + try { + transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", path); + } catch (Error e) { + warning("Error getting transfer proxy: %s", e.message); + } + + if (transfer.name == null) return; + + file_receiver = new FileReceiver(this); + file_receivers.append(file_receiver); + + file_receiver.destroy.connect(() => { + file_receivers.foreach((receiver) => { + if (receiver.transfer.session == file_receiver.session_path) { + file_receivers.remove_link(file_receivers.find(receiver)); + } + }); + }); + + Bluetooth.Device device = manager.get_device(address); + file_receiver.set_transfer(device.alias ?? get_name_for_device(device), device.icon, path); + } + + private void response_notify(string address, ObjectPath object_path) { + Bluetooth.Device device = manager.get_device(address); + + try { + transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", object_path); + } catch (Error e) { + warning("Error getting transfer proxy: %s", e.message); + } + + var notification = new Notification("Bluetooth"); + notification.set_icon(new ThemedIcon(device.icon)); + + if (reject_if_exists(transfer.name, transfer.size)) { + notification.set_title(_("Rejected file")); + notification.set_body(_("File already exists: %s").printf(transfer.name)); + send_notification("org.buddiesofbudgie.bluetooth", notification); + Idle.add(() => { + activate_action("btcancel", new Variant.string("Cancel")); + return Source.REMOVE; + }); + + return; + } + + // Create a notification prompting the user what to do + notification.set_priority(NotificationPriority.URGENT); + notification.set_title(_("Receiving file")); + notification.set_body(_("Device '%s' wants to send a file: %s %s").printf(device.alias, transfer.name, format_size(transfer.size))); + notification.add_button( + _("Accept"), + Action.print_detailed_name("app.btaccept", new Variant.string("Accept")) + ); + notification.add_button( + _("Reject"), + Action.print_detailed_name("app.btcancel", new Variant.string("Cancel")) + ); + + send_notification("org.buddiesofbudgie.bluetooth", notification); + } + + private bool reject_if_exists(string name, uint64 size) { + var input_path = Path.build_filename(Environment.get_user_special_dir(UserDirectory.DOWNLOAD), name); + var input_file = File.new_for_path(input_path); + uint64 file_size = 0; + + if (input_file.query_exists()) { + try { + var file_info = input_file.query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE, null); + file_size = file_info.get_size(); + } catch (Error e) { + warning("Error getting file size: %s", e.message); + } + } + + return size == file_size && input_file.query_exists(); + } + + private string get_name_for_device(Bluetooth.Device device) { + switch (device.icon) { + case "audio-card": + return _("Speaker"); + case "input-gaming": + return _("Controller"); + case "input-keyboard": + return _("Keyboard"); + case "input-mouse": + return _("Mouse"); + case "input-tablet": + return _("Tablet"); + case "input-touchpad": + return _("Touchpad"); + case "phone": + return _("Phone"); + default: + return device.address; + } + } } public static int main(string[] args) { diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala new file mode 100644 index 000000000..bf5a87208 --- /dev/null +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -0,0 +1,253 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +public class FileReceiver : Gtk.Dialog { + public string session_path { get; set; } + + public Bluetooth.Obex.Transfer transfer; + + private Gtk.ProgressBar progress_bar; + private Gtk.Label device_label; + private Gtk.Label directory_label; + private Gtk.Label progress_label; + private Gtk.Label filename_label; + private Gtk.Label rate_label; + private Gtk.Image device_image; + + private Notification notification; + + private string folder_path = ""; + private int start_time = 0; + private uint64 total_size = 0; + + public FileReceiver(Gtk.Application application) { + Object(application: application, resizable: false); + } + + construct { + notification = new Notification("Bluetooth"); + notification.set_priority(NotificationPriority.NORMAL); + + var icon_image = new Gtk.Image.from_icon_name ("bluetooth-active", Gtk.IconSize.DIALOG) { + valign = Gtk.Align.END, + halign = Gtk.Align.END, + }; + + device_image = new Gtk.Image() { + valign = Gtk.Align.END, + halign = Gtk.Align.END, + }; + + var overlay = new Gtk.Overlay(); + overlay.add(icon_image); + overlay.add_overlay(device_image); + + device_label = new Gtk.Label(null) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + device_label.get_style_context().add_class("primary"); + + directory_label = new Gtk.Label(null) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + filename_label = new Gtk.Label(Markup.printf_escaped("%s:", _("File name"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + rate_label = new Gtk.Label(Markup.printf_escaped("%s:", _("Transfer rate"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + progress_bar = new Gtk.ProgressBar() { + hexpand = true, + }; + + progress_label = new Gtk.Label(null) { + max_width_chars = 45, + hexpand = false, + wrap = true, + xalign = 0, + }; + + var message_grid = new Gtk.Grid() { + column_spacing = 0, + width_request = 450, + margin_start = 10, + margin_end = 15 + }; + + message_grid.attach(overlay, 0, 0, 1, 3); + message_grid.attach(device_label, 1, 0, 1, 1); + message_grid.attach(directory_label, 1, 1, 1, 1); + message_grid.attach(filename_label, 1, 2, 1, 1); + message_grid.attach(rate_label, 1, 3, 1, 1); + message_grid.attach(progress_bar, 1, 4, 1, 1); + message_grid.attach(progress_label, 1, 5, 1, 1); + + get_content_area().add(message_grid); + + // Now add the dialog buttons + add_button(_("Close"), Gtk.ResponseType.CLOSE); + var reject_button = add_button(_("Reject"), Gtk.ResponseType.REJECT); + reject_button.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + // Hook up the responses + response.connect((response_id) => { + if (response_id == Gtk.ResponseType.REJECT) { + // Cancel the current transfer if it is active + try { + transfer.cancel(); + } catch (Error e) { + warning("Error rejecting Bluetooth transfer: %s", e.message); + } + + destroy(); + } else { + // Close button clicked, hide + hide_on_delete(); + } + }); + + delete_event.connect(() => { + if (transfer.status == "active") { + return hide_on_delete(); + } else { + return false; + } + }); + } + + public void set_transfer(string device_name, string device_icon, string path) { + device_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), device_name)); + directory_label.set_markup(Markup.printf_escaped(_("To: %s"), Environment.get_user_special_dir (UserDirectory.DOWNLOAD))); + device_image.set_from_gicon(new ThemedIcon(device_icon ?? "bluetooth-active"), Gtk.IconSize.LARGE_TOOLBAR); + start_time = (int) get_real_time(); + + try { + transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", path); + ((DBusProxy) transfer).g_properties_changed.connect((changed, invalid) => { + transfer_progress(); + }); + + total_size = transfer.size; + session_path = transfer.session; + filename_label.set_markup(Markup.printf_escaped (_("File name: %s"), transfer.name)); + } catch (Error e) { + warning("Error accepting Bluetooth file transfer: %s", e.message); + } + } + + private void transfer_progress() { + switch (transfer.status) { + case "error": + notification.set_icon(device_image.gicon); + notification.set_title(_("File transfer failed")); + notification.set_body(Markup.printf_escaped(_("%s File: %s not received"), device_label.get_label(), transfer.name)); + ((Gtk.Window) get_toplevel()).application.send_notification("org.buddiesofbudgie.bluetooth", notification); + destroy(); + break; + case "queued": + break; + case "active": + on_transfer_progress(transfer.transferred); + break; + case "complete": + try { + move_to_downloads(transfer.filename); + } catch (Error e) { + notification.set_icon(device_image.gicon); + notification.set_title(_("File transfer failed")); + notification.set_body(_("File '%s' from %s not received: %s".printf(transfer.name, device_label.get_label(), e.message))); + ((Gtk.Window) get_toplevel()).application.send_notification("org.buddiesofbudgie.bluetooth", notification); + warning("Error saving transferred file: %s", e.message); + } + destroy(); + break; + } + } + + private void move_to_downloads(string path) throws Error { + var source = File.new_for_path(path); + var file_name = Path.build_filename(Environment.get_user_special_dir(UserDirectory.DOWNLOAD), source.get_basename()); + var dest = get_save_name(file_name); + + source.move(dest, FileCopyFlags.ALL_METADATA); + + notification.set_icon(device_image.gicon); + notification.set_title(_("File transferred successfully")); + notification.set_body(Markup.printf_escaped(_("%s Saved to: %s"), device_label.get_label(), dest.get_path())); + ((Gtk.Window) get_toplevel()).application.send_notification("org.buddiesofbudgie.bluetooth", notification); + } + + private File? get_save_name(string uri) { + var file = File.new_for_path(uri); + + if (!file.query_exists()) return file; + + var base_name = file.get_basename(); + var ext_index = base_name.last_index_of("."); + var name = ext_index == -1 ? base_name : base_name.substring(0, ext_index); + var ext = ext_index == -1 ? "" : base_name.substring(ext_index + 1); + var time = new DateTime.now_local().format_iso8601(); + + return File.new_for_path(name + " " + time + ext); + } + + private void on_transfer_progress(uint64 transferred) { + progress_bar.fraction = (double) transferred / (double) total_size; + int current_time = (int) get_real_time(); + int elapsed_time = (current_time - start_time) / 1000000; + if (current_time < start_time + 1000000) return; + if (elapsed_time == 0) return; + + uint64 transfer_rate = transferred / elapsed_time; + if (transfer_rate == 0) return; + + rate_label.label = Markup.printf_escaped(_("Transfer rate: %s"), format_size(transfer_rate)); + uint64 remaining_time = (total_size - transferred) / transfer_rate; + progress_label.label = _("%s of %s received. Time remaining: %s").printf(format_size(transferred), format_size(total_size), format_time((int) remaining_time)); + } + + private string format_time(int seconds) { + if (seconds < 0) seconds = 0; + if (seconds < 60) return ngettext("%d second", "%d seconds", seconds).printf(seconds); + + int minutes; + if (seconds < 60 * 60) { + minutes = (seconds + 30) / 60; + return ngettext("%d minute", "%d minutes", minutes).printf(minutes); + } + + int hours = seconds / (60 * 60); + if (seconds < 60 * 60 * 4) { + minutes = (seconds - hours * 60 * 60 + 30) / 60; + string h = ngettext("%u hour", "%u hours", hours).printf(hours); + string m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + ///TRANSLATORS: For example "1 hour, 8 minutes". + return _("%s, %s").printf(h, m); + } + + return ngettext("about %d hour", "about %d hours", hours).printf(hours); + } +} diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala index 08f2df0ee..3d7b35654 100644 --- a/src/dialogs/sendto/Dialog/FileSender.vala +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -128,7 +128,7 @@ public class FileSender : Gtk.Dialog { warning("Error cancelling Bluetooth transfer: %s", e.message); } - // TODO: remove_session.begin(); + remove_session.begin(); } destroy(); @@ -373,9 +373,9 @@ public class FileSender : Gtk.Dialog { uint64 transfer_rate = transferred / elapsed_time; if (transfer_rate == 0) return; - rate_label.label = Markup.printf_escaped (_("Transfer rate: %s"), format_size(transfer_rate)); + rate_label.label = Markup.printf_escaped(_("Transfer rate: %s"), format_size(transfer_rate)); uint64 remaining_time = (total_size - transferred) / transfer_rate; - progress_label.label = _("(%i/%i) %s of %s sent. Time remaining: %s").printf (current_file, total_files, format_size(transferred), format_size(total_size), format_time((int) remaining_time)); + progress_label.label = _("(%i/%i) %s of %s sent. Time remaining: %s").printf(current_file, total_files, format_size(transferred), format_size(total_size), format_time((int) remaining_time)); } private string format_time(int seconds) { diff --git a/src/dialogs/sendto/Services/ObexAgent.vala b/src/dialogs/sendto/Services/ObexAgent.vala index 22443081d..c39d9d96c 100644 --- a/src/dialogs/sendto/Services/ObexAgent.vala +++ b/src/dialogs/sendto/Services/ObexAgent.vala @@ -36,7 +36,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { private void on_name_get(GLib.DBusConnection conn) { try { - conn.register_object ("/org/bluez/obex/budgie", this); + conn.register_object("/org/bluez/obex/budgie", this); } catch (Error e) { error (e.message); } diff --git a/src/dialogs/sendto/meson.build b/src/dialogs/sendto/meson.build index 1ee04c1eb..687275676 100644 --- a/src/dialogs/sendto/meson.build +++ b/src/dialogs/sendto/meson.build @@ -1,6 +1,7 @@ sendto_sources = [ 'Application.vala', 'Dialog/DeviceRow.vala', + 'Dialog/FileReceiver.vala', 'Dialog/FileSender.vala', 'Dialog/ScanDialog.vala', 'Services/Adapter.vala', From 1f5160689b5af143cc943f4428d0882d448c9840 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 19 Aug 2023 15:30:45 -0400 Subject: [PATCH 63/81] bluetooth/sendto: Fix issues with no transfer dialog windows being shown Also fixes saving received files to the Downloads folder. Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 148 +++++++----------- src/dialogs/sendto/Dialog/FileReceiver.vala | 25 ++- src/dialogs/sendto/Services/ObexAgent.vala | 53 +++++-- .../applets/status/BluetoothIndicator.vala | 55 ++++++- .../applets/status/BluetoothObexManager.vala | 12 +- 5 files changed, 176 insertions(+), 117 deletions(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index af0848406..e93ca1f44 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -112,8 +112,6 @@ public class SendtoApplication : Gtk.Application { // Clear our pointer when the scan dialog is destroyed scan_dialog.destroy.connect(() => { scan_dialog = null; - - if (!silent) quit(); }); // Send the files when a device has been selected @@ -124,15 +122,12 @@ public class SendtoApplication : Gtk.Application { file_senders.append(file_sender); file_sender.show_all(); file_sender.destroy.connect(() => { - file_senders.foreach((sender) => { - if (sender.device == file_sender.device) { - file_senders.remove_link(file_senders.find(sender)); - } - }); + file_senders.remove_link(file_senders.find(file_sender)); }); } }); + // Cleanup arg_files = {}; send = false; @@ -142,70 +137,70 @@ public class SendtoApplication : Gtk.Application { protected override void activate() { if (silent) { if (active_once) { - release(); + release(); // Allow normal exit if `activate()` has already been called once } - hold(); + hold(); // Prevent normal application exit if silent silent = false; } - if (manager == null) { - file_receivers = new List(); - file_senders = new List(); - - manager = new Bluetooth.ObjectManager(); - manager.notify["has-object"].connect(() => { - var build_path = Path.build_filename(Environment.get_home_dir(), ".local", "share", "contractor"); - var file = File.new_for_path( - Path.build_filename( - build_path, - Environment.get_application_name() + ".contract" - ) - ); - var file_exists = file.query_exists(); - - // Create the parent directory for the contract file if it doesn't exist - if (!File.new_for_path(build_path).query_exists()) { - DirUtils.create(build_path, 0700); - } + if (manager != null) return; + + file_receivers = new List(); + file_senders = new List(); + + manager = new Bluetooth.ObjectManager(); + manager.notify["has-object"].connect(() => { + var build_path = Path.build_filename(Environment.get_home_dir(), ".local", "share", "contractor"); + var file = File.new_for_path( + Path.build_filename( + build_path, + Environment.get_application_name() + ".contract" + ) + ); + var file_exists = file.query_exists(); + + // Create the parent directory for the contract file if it doesn't exist + if (!File.new_for_path(build_path).query_exists()) { + DirUtils.create(build_path, 0700); + } - // If we have Bluetooth devices, create our Obex Agent and contract file - if (manager.has_object) { - // Create our Obex Agent if we haven't been activated yet - if (!active_once) { - agent = new Bluetooth.Obex.Agent(); - agent.transfer_view.connect(dialog_active); - agent.response_accepted.connect(response_accepted); - agent.response_notify.connect(response_notify); - active_once = true; - } + // If we have Bluetooth devices, create our Obex Agent and contract file + if (manager.has_object) { + // Create our Obex Agent if we haven't been activated yet + if (!active_once) { + agent = new Bluetooth.Obex.Agent(); + agent.transfer_view.connect(dialog_active); + agent.response_accepted.connect(response_accepted); + agent.response_notify.connect(response_notify); + active_once = true; + } - // Create and write to our Obex contract file if it doesn't exist - if (!file_exists) { - var keyfile = new KeyFile(); - keyfile.set_string("Contractor Entry", "Name", _("Send Files via Bluetooth")); - keyfile.set_string("Contractor Entry", "Icon", "bluetooth-active"); - keyfile.set_string("Contractor Entry", "Description", _("Send files to device…")); - keyfile.set_string("Contractor Entry", "Exec", "org.buddiesofbudgie.sendto -f %F"); - keyfile.set_string("Contractor Entry", "MimeType", "!inode;"); - - try { - keyfile.save_to_file(file.get_path()); - } catch (Error e) { - critical("Error saving contract file: %s", e.message); - } + // Create and write to our Obex contract file if it doesn't exist + if (!file_exists) { + var keyfile = new KeyFile(); + keyfile.set_string("Contractor Entry", "Name", _("Send Files via Bluetooth")); + keyfile.set_string("Contractor Entry", "Icon", "bluetooth-active"); + keyfile.set_string("Contractor Entry", "Description", _("Send files to device…")); + keyfile.set_string("Contractor Entry", "Exec", "org.buddiesofbudgie.sendto -f %F"); + keyfile.set_string("Contractor Entry", "MimeType", "!inode;"); + + try { + keyfile.save_to_file(file.get_path()); + } catch (Error e) { + critical("Error saving contract file: %s", e.message); } - } else { - // Delete the contract file if it exists - if (file_exists) { - try { - file.delete(); - } catch (Error e) { - critical("Error deleting old contract file: %s", e.message); - } + } + } else { + // Delete the contract file if it exists + if (file_exists) { + try { + file.delete(); + } catch (Error e) { + critical("Error deleting old contract file: %s", e.message); } } - }); - } + } + }); } private void dialog_active(string session_path) { @@ -254,15 +249,11 @@ public class SendtoApplication : Gtk.Application { file_receivers.append(file_receiver); file_receiver.destroy.connect(() => { - file_receivers.foreach((receiver) => { - if (receiver.transfer.session == file_receiver.session_path) { - file_receivers.remove_link(file_receivers.find(receiver)); - } - }); + file_receivers.remove_link(file_receivers.find(file_receiver)); }); Bluetooth.Device device = manager.get_device(address); - file_receiver.set_transfer(device.alias ?? get_name_for_device(device), device.icon, path); + file_receiver.set_transfer(device, path); } private void response_notify(string address, ObjectPath object_path) { @@ -321,27 +312,6 @@ public class SendtoApplication : Gtk.Application { return size == file_size && input_file.query_exists(); } - - private string get_name_for_device(Bluetooth.Device device) { - switch (device.icon) { - case "audio-card": - return _("Speaker"); - case "input-gaming": - return _("Controller"); - case "input-keyboard": - return _("Keyboard"); - case "input-mouse": - return _("Mouse"); - case "input-tablet": - return _("Tablet"); - case "input-touchpad": - return _("Touchpad"); - case "phone": - return _("Phone"); - default: - return device.address; - } - } } public static int main(string[] args) { diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala index bf5a87208..e9eaec8cb 100644 --- a/src/dialogs/sendto/Dialog/FileReceiver.vala +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -10,6 +10,7 @@ */ public class FileReceiver : Gtk.Dialog { + public Bluetooth.Device device { get; set; } public string session_path { get; set; } public Bluetooth.Obex.Transfer transfer; @@ -24,7 +25,7 @@ public class FileReceiver : Gtk.Dialog { private Notification notification; - private string folder_path = ""; + private string file_name = ""; private int start_time = 0; private uint64 total_size = 0; @@ -138,10 +139,12 @@ public class FileReceiver : Gtk.Dialog { }); } - public void set_transfer(string device_name, string device_icon, string path) { - device_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), device_name)); + public void set_transfer(Bluetooth.Device device, string path) { + this.device = device; + + device_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), device.alias)); directory_label.set_markup(Markup.printf_escaped(_("To: %s"), Environment.get_user_special_dir (UserDirectory.DOWNLOAD))); - device_image.set_from_gicon(new ThemedIcon(device_icon ?? "bluetooth-active"), Gtk.IconSize.LARGE_TOOLBAR); + device_image.set_from_gicon(new ThemedIcon(device.icon ?? "bluetooth-active"), Gtk.IconSize.LARGE_TOOLBAR); start_time = (int) get_real_time(); try { @@ -163,22 +166,28 @@ public class FileReceiver : Gtk.Dialog { case "error": notification.set_icon(device_image.gicon); notification.set_title(_("File transfer failed")); - notification.set_body(Markup.printf_escaped(_("%s File: %s not received"), device_label.get_label(), transfer.name)); + notification.set_body(_("File '%s' not received from %s").printf(transfer.name, device.alias)); ((Gtk.Window) get_toplevel()).application.send_notification("org.buddiesofbudgie.bluetooth", notification); destroy(); break; case "queued": break; case "active": + // Save the file name here because it won't be available later + var name = transfer.filename; + if (name != null) { + file_name = name; + } + // Update the transfer progress UI on_transfer_progress(transfer.transferred); break; case "complete": try { - move_to_downloads(transfer.filename); + move_to_downloads(file_name); } catch (Error e) { notification.set_icon(device_image.gicon); notification.set_title(_("File transfer failed")); - notification.set_body(_("File '%s' from %s not received: %s".printf(transfer.name, device_label.get_label(), e.message))); + notification.set_body(_("File '%s' from %s not received: %s".printf(transfer.name, device.alias, e.message))); ((Gtk.Window) get_toplevel()).application.send_notification("org.buddiesofbudgie.bluetooth", notification); warning("Error saving transferred file: %s", e.message); } @@ -196,7 +205,7 @@ public class FileReceiver : Gtk.Dialog { notification.set_icon(device_image.gicon); notification.set_title(_("File transferred successfully")); - notification.set_body(Markup.printf_escaped(_("%s Saved to: %s"), device_label.get_label(), dest.get_path())); + notification.set_body(_("Saved file from %s to '%s'").printf(device.alias, dest.get_path())); ((Gtk.Window) get_toplevel()).application.send_notification("org.buddiesofbudgie.bluetooth", notification); } diff --git a/src/dialogs/sendto/Services/ObexAgent.vala b/src/dialogs/sendto/Services/ObexAgent.vala index c39d9d96c..07afdec54 100644 --- a/src/dialogs/sendto/Services/ObexAgent.vala +++ b/src/dialogs/sendto/Services/ObexAgent.vala @@ -17,7 +17,7 @@ public errordomain BluezObexError { [DBus (name = "org.bluez.obex.Agent1")] public class Bluetooth.Obex.Agent : GLib.Object { - /*one confirmation for many files in one session */ + /* one confirmation for many files in one session */ private GLib.ObjectPath many_files; public signal void response_notify(string address, GLib.ObjectPath objectpath); @@ -38,7 +38,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { try { conn.register_object("/org/bluez/obex/budgie", this); } catch (Error e) { - error (e.message); + error("Error registering DBus name: %s", e.message); } } @@ -46,27 +46,52 @@ public class Bluetooth.Obex.Agent : GLib.Object { transfer_view(session_path); } + /** + * release: + * + * This method gets called when the service daemon + * unregisters the agent. An agent can use it to do + * cleanup tasks. There is no need to unregister the + * agent, because when this method gets called it has + * already been unregistered. + */ public void release() throws GLib.Error {} - public async string authorize_push(GLib.ObjectPath objectpath) throws Error { + /** + * authorize_push: + * @object_path: The path to a Bluez #Transfer object. + * + * This method gets called when the service daemon + * needs to accept/reject a Bluetooth object push request. + * + * Returns: The full path (including the filename) or the + * folder name suffixed with '/' where the object shall + * be stored. The transfer object will contain a Filename + * property that contains the default location and name + * that can be returned. + */ + public async string authorize_push(GLib.ObjectPath object_path) throws Error { SourceFunc callback = authorize_push.callback; BluezObexError? obex_error = null; - Bluetooth.Obex.Transfer transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", objectpath); + Bluetooth.Obex.Transfer transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", object_path); if (transfer.name == null) { throw new BluezObexError.REJECTED("Authorize Reject"); } Bluetooth.Obex.Session session = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", transfer.session); + + // Register application action to accept a file transfer var accept_action = new SimpleAction("btaccept", VariantType.STRING); GLib.Application.get_default().add_action(accept_action); accept_action.activate.connect((parameter) => { - response_accepted(session.destination, objectpath); + response_accepted(session.destination, object_path); if (callback != null) { Idle.add((owned) callback); } }); + // Register application action to reject a file transfer var cancel_action = new SimpleAction("btcancel", VariantType.STRING); GLib.Application.get_default().add_action(cancel_action); cancel_action.activate.connect((parameter) => { @@ -77,26 +102,36 @@ public class Bluetooth.Obex.Agent : GLib.Object { } }); - if (many_files == objectpath) { + // Automatically accept the transfer if there are multiple files for + // the one transfer + if (many_files == object_path) { Idle.add(()=>{ - response_accepted(session.destination, objectpath); + response_accepted(session.destination, object_path); if (callback != null) { Idle.add((owned) callback); } return GLib.Source.REMOVE; }); } else { - response_notify(session.destination, objectpath); + // Not multple files, ask to accept or reject + response_notify(session.destination, object_path); } yield; if (obex_error != null) throw obex_error; - many_files = objectpath; + many_files = object_path; return transfer.name; } + /** + * cancel: + * + * This method gets called to indicate that the agent + * request failed before a reply was returned. It cancels + * the previous request. + */ public void cancel() throws GLib.Error { response_canceled(); } diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index b7c3a7adf..331b137a0 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -27,6 +27,7 @@ public class BluetoothIndicator : Bin { private Label? placeholder_sublabel = null; private BluetoothClient client; + private ObexManager obex_manager; private ulong switch_handler_id; @@ -40,6 +41,7 @@ public class BluetoothIndicator : Bin { // Create our Bluetooth client client = new BluetoothClient(); + obex_manager = new ObexManager(); client.device_added.connect((device) => { // Remove any existing rows for this device @@ -253,7 +255,7 @@ public class BluetoothIndicator : Bin { private void add_device(Device1 device) { debug("Bluetooth device added: %s", device.alias); - var widget = new BTDeviceRow(device); + var widget = new BTDeviceRow(device, obex_manager); widget.properties_updated.connect(() => { devices_box.invalidate_filter(); @@ -335,6 +337,9 @@ public class BluetoothIndicator : Bin { * Widget for displaying a Bluetooth device in a ListBox. */ public class BTDeviceRow : ListBoxRow { + private const string OBEX_AGENT = "org.bluez.obex.Agent1"; + private const string OBEX_PATH = "/org/bluez/obex/budgie"; + private Image? image = null; private Label? name_label = null; private Revealer? battery_revealer = null; @@ -350,6 +355,7 @@ public class BTDeviceRow : ListBoxRow { private ProgressBar? progress_bar = null; public Device1 device { get; construct; } + public ObexManager obex_manager { get; construct; } public Transfer transfer; private ulong up_handler_id = 0; @@ -379,7 +385,6 @@ public class BTDeviceRow : ListBoxRow { get_style_context().add_class("bluetooth-device-row"); // Obex manager for file transfers - var obex_manager = new ObexManager(); obex_manager.transfer_active.connect(transfer_active); obex_manager.transfer_added.connect(transfer_added); obex_manager.transfer_removed.connect(transfer_removed); @@ -480,6 +485,8 @@ public class BTDeviceRow : ListBoxRow { reveal_child = false, transition_duration = 250, transition_type = RevealerTransitionType.SLIDE_DOWN, + margin_left = 4, + margin_right = 4, }; progress_label = new Label(null) { @@ -491,6 +498,8 @@ public class BTDeviceRow : ListBoxRow { progress_bar = new ProgressBar() { hexpand = true, + margin_top = 6, + margin_bottom = 6, }; file_label = new Label(null) { @@ -505,6 +514,7 @@ public class BTDeviceRow : ListBoxRow { progress_grid.attach(file_label, 0, 0); progress_grid.attach(progress_bar, 0, 1); progress_grid.attach(progress_label, 0, 2); + progress_revealer.add(progress_grid); // Signals ((DBusProxy) device).g_properties_changed.connect(update_status); @@ -515,17 +525,20 @@ public class BTDeviceRow : ListBoxRow { grid.attach(button_box, 4, 0, 1, 2); grid.attach(status_box, 2, 1, 2, 1); grid.attach(battery_revealer, 2, 2, 1, 1); - grid.attach(progress_revealer, 1, 3); - box.pack_start(grid); + var box_grid = new Grid(); + box_grid.attach(grid, 0, 0); + box_grid.attach(progress_revealer, 0, 1); + + box.pack_start(box_grid); add(box); show_all(); update_status(); } - public BTDeviceRow(Device1 device) { - Object(device: device); + public BTDeviceRow(Device1 device, ObexManager obex_manager) { + Object(device: device, obex_manager: obex_manager); } private void hide_progress_revealer() { @@ -538,6 +551,28 @@ public class BTDeviceRow : ListBoxRow { * device depending on its current connection state. */ public async void toggle_connection() { + // Show transfer progress dialog on click if a transfer is + // in progress + if (progress_revealer.child_revealed) { + try { + var conn = yield Bus.get(BusType.SESSION); + yield conn.call( + OBEX_AGENT, + OBEX_PATH, + OBEX_AGENT, + "TransferActive", + new Variant("(s)", transfer.session), + null, + DBusCallFlags.NONE, + -1 + ); + } catch (Error e) { + warning("Error activating Bluetooth file transfer: %s", e.message); + } + + return; + } + if (spinner.active) return; spinner.active = true; @@ -655,11 +690,12 @@ public class BTDeviceRow : ListBoxRow { // Update the progress bar progress_bar.fraction = (double) transfer.transferred / (double) transfer.size; progress_revealer.reveal_child = true; + activatable = true; // Update the filename label var name = transfer.name; - if (name == null) { - file_label.label = _("Filename: %s").printf(Markup.escape_text(name)); + if (name != null) { + file_label.set_markup(_("Filename: %s").printf(Markup.escape_text(name))); } // Update the progress label @@ -680,6 +716,9 @@ public class BTDeviceRow : ListBoxRow { break; case "complete": hide_progress_revealer(); + if (device.connected) { + activatable = false; + } break; } } diff --git a/src/panel/applets/status/BluetoothObexManager.vala b/src/panel/applets/status/BluetoothObexManager.vala index bbc7ef3f1..b6a9176bd 100644 --- a/src/panel/applets/status/BluetoothObexManager.vala +++ b/src/panel/applets/status/BluetoothObexManager.vala @@ -17,8 +17,10 @@ public class ObexManager : Object { public signal void transfer_active(string address); private DBusObjectManager object_manager; + private HashTable active_transfers; construct { + active_transfers = new HashTable(direct_hash, direct_equal); create_manager.begin(); } @@ -89,11 +91,11 @@ public class ObexManager : Object { critical("Error getting Obex session proxy: %s", e.message); } - transfer_added(session.destination, transfer); - + active_transfers[transfer] = session.destination; ((DBusProxy) transfer).g_properties_changed.connect((changed, invalid) => { transfer_active(session.destination); }); + transfer_added(session.destination, transfer); } } @@ -102,7 +104,11 @@ public class ObexManager : Object { */ private void interface_removed(DBusObject obj, DBusInterface iface) { if (iface is Transfer) { - transfer_removed(iface as Transfer); + unowned Transfer transfer = (Transfer) iface; + if (active_transfers.contains(transfer)) { + active_transfers.remove(transfer); + } + transfer_removed(transfer); } } } From c65652d97ac584f97309d8f781a02b8dec6516e7 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 19 Aug 2023 16:22:54 -0400 Subject: [PATCH 64/81] sendto: Correct attribution in file headers Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 2 +- src/dialogs/sendto/Dialog/DeviceRow.vala | 2 +- src/dialogs/sendto/Dialog/FileReceiver.vala | 2 +- src/dialogs/sendto/Dialog/FileSender.vala | 2 +- src/dialogs/sendto/Dialog/ScanDialog.vala | 2 +- src/dialogs/sendto/Services/Adapter.vala | 2 +- src/dialogs/sendto/Services/Device.vala | 2 +- src/dialogs/sendto/Services/Manager.vala | 2 +- src/dialogs/sendto/Services/ObexAgent.vala | 2 +- src/dialogs/sendto/Services/Session.vala | 2 +- src/dialogs/sendto/Services/Transfer.vala | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index e93ca1f44..56f10efc9 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Dialog/DeviceRow.vala b/src/dialogs/sendto/Dialog/DeviceRow.vala index 1fbb9d3bd..b341d70fa 100644 --- a/src/dialogs/sendto/Dialog/DeviceRow.vala +++ b/src/dialogs/sendto/Dialog/DeviceRow.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala index e9eaec8cb..49ac77593 100644 --- a/src/dialogs/sendto/Dialog/FileReceiver.vala +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala index 3d7b35654..f34edc745 100644 --- a/src/dialogs/sendto/Dialog/FileSender.vala +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Dialog/ScanDialog.vala b/src/dialogs/sendto/Dialog/ScanDialog.vala index 77525dc25..b37aa1107 100644 --- a/src/dialogs/sendto/Dialog/ScanDialog.vala +++ b/src/dialogs/sendto/Dialog/ScanDialog.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Services/Adapter.vala b/src/dialogs/sendto/Services/Adapter.vala index 046f8cff0..142b13b31 100644 --- a/src/dialogs/sendto/Services/Adapter.vala +++ b/src/dialogs/sendto/Services/Adapter.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Services/Device.vala b/src/dialogs/sendto/Services/Device.vala index 0a3197291..af0fc3c8f 100644 --- a/src/dialogs/sendto/Services/Device.vala +++ b/src/dialogs/sendto/Services/Device.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Services/Manager.vala b/src/dialogs/sendto/Services/Manager.vala index c72d6e75c..aeb4b5473 100644 --- a/src/dialogs/sendto/Services/Manager.vala +++ b/src/dialogs/sendto/Services/Manager.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Services/ObexAgent.vala b/src/dialogs/sendto/Services/ObexAgent.vala index 07afdec54..95df909eb 100644 --- a/src/dialogs/sendto/Services/ObexAgent.vala +++ b/src/dialogs/sendto/Services/ObexAgent.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Services/Session.vala b/src/dialogs/sendto/Services/Session.vala index eb37057dd..50d8a1a94 100644 --- a/src/dialogs/sendto/Services/Session.vala +++ b/src/dialogs/sendto/Services/Session.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/dialogs/sendto/Services/Transfer.vala b/src/dialogs/sendto/Services/Transfer.vala index 2f01ebd1e..d897e6e6c 100644 --- a/src/dialogs/sendto/Services/Transfer.vala +++ b/src/dialogs/sendto/Services/Transfer.vala @@ -1,7 +1,7 @@ /* * This file is part of budgie-desktop * - * Copyright Budgie Desktop Developers + * Copyright Budgie Desktop Developers, elementary LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From 181545eb7e60574efe862480af00bdf197fbf765 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Thu, 24 Aug 2023 15:16:38 -0400 Subject: [PATCH 65/81] sendto: Add spacing in dialogs Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/FileReceiver.vala | 10 ++++++++-- src/dialogs/sendto/Dialog/FileSender.vala | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala index 49ac77593..a0377f779 100644 --- a/src/dialogs/sendto/Dialog/FileReceiver.vala +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -47,7 +47,9 @@ public class FileReceiver : Gtk.Dialog { halign = Gtk.Align.END, }; - var overlay = new Gtk.Overlay(); + var overlay = new Gtk.Overlay() { + margin_right = 12, + }; overlay.add(icon_image); overlay.add_overlay(device_image); @@ -82,6 +84,8 @@ public class FileReceiver : Gtk.Dialog { progress_bar = new Gtk.ProgressBar() { hexpand = true, + margin_top = 4, + margin_bottom = 4, }; progress_label = new Gtk.Label(null) { @@ -89,10 +93,12 @@ public class FileReceiver : Gtk.Dialog { hexpand = false, wrap = true, xalign = 0, + margin_bottom = 4, }; var message_grid = new Gtk.Grid() { column_spacing = 0, + row_spacing = 4, width_request = 450, margin_start = 10, margin_end = 15 @@ -233,7 +239,7 @@ public class FileReceiver : Gtk.Dialog { uint64 transfer_rate = transferred / elapsed_time; if (transfer_rate == 0) return; - rate_label.label = Markup.printf_escaped(_("Transfer rate: %s"), format_size(transfer_rate)); + rate_label.label = Markup.printf_escaped(_("Transfer rate: %s / s"), format_size(transfer_rate)); uint64 remaining_time = (total_size - transferred) / transfer_rate; progress_label.label = _("%s of %s received. Time remaining: %s").printf(format_size(transferred), format_size(total_size), format_time((int) remaining_time)); } diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala index f34edc745..992bfb864 100644 --- a/src/dialogs/sendto/Dialog/FileSender.vala +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -51,7 +51,9 @@ public class FileSender : Gtk.Dialog { halign = Gtk.Align.END, }; - var overlay = new Gtk.Overlay(); + var overlay = new Gtk.Overlay() { + margin_right = 12, + }; overlay.add(icon_image); overlay.add_overlay(icon_label); @@ -86,6 +88,8 @@ public class FileSender : Gtk.Dialog { progress_bar = new Gtk.ProgressBar() { hexpand = true, + margin_top = 4, + margin_bottom = 4, }; progress_label = new Gtk.Label(null) { @@ -93,10 +97,12 @@ public class FileSender : Gtk.Dialog { hexpand = false, wrap = true, xalign = 0, + margin_bottom = 4, }; var message_grid = new Gtk.Grid() { column_spacing = 0, + row_spacing = 4, width_request = 450, margin_start = 10, margin_end = 15 @@ -373,7 +379,7 @@ public class FileSender : Gtk.Dialog { uint64 transfer_rate = transferred / elapsed_time; if (transfer_rate == 0) return; - rate_label.label = Markup.printf_escaped(_("Transfer rate: %s"), format_size(transfer_rate)); + rate_label.label = Markup.printf_escaped(_("Transfer rate: %s / s"), format_size(transfer_rate)); uint64 remaining_time = (total_size - transferred) / transfer_rate; progress_label.label = _("(%i/%i) %s of %s sent. Time remaining: %s").printf(current_file, total_files, format_size(transferred), format_size(total_size), format_time((int) remaining_time)); } From 0aef339e487b9c03c201bebd9d87e081df1ec20e Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Thu, 24 Aug 2023 15:17:07 -0400 Subject: [PATCH 66/81] sendto: Cancel the transfer on cancel/reject This means that the sending device will no longer still send the file, even if the transfer was rejected. Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 18 ++++++++++++++++++ src/dialogs/sendto/Services/ObexAgent.vala | 12 ++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index 56f10efc9..3b3cfc842 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -171,6 +171,7 @@ public class SendtoApplication : Gtk.Application { agent = new Bluetooth.Obex.Agent(); agent.transfer_view.connect(dialog_active); agent.response_accepted.connect(response_accepted); + agent.response_canceled.connect(response_canceled); agent.response_notify.connect(response_notify); active_once = true; } @@ -256,6 +257,23 @@ public class SendtoApplication : Gtk.Application { file_receiver.set_transfer(device, path); } + private void response_canceled(ObjectPath? path = null) { + try { + Bluetooth.Obex.Transfer? transfer = null; + + if (path == null) { + var last_receiver = file_receivers.first().data as FileReceiver; + transfer = last_receiver.transfer; + } else { + transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", path); + } + + transfer.cancel(); + } catch (Error e) { + warning("Error cancelling file transfer: %s", e.message); + } + } + private void response_notify(string address, ObjectPath object_path) { Bluetooth.Device device = manager.get_device(address); diff --git a/src/dialogs/sendto/Services/ObexAgent.vala b/src/dialogs/sendto/Services/ObexAgent.vala index 95df909eb..e37ca2da7 100644 --- a/src/dialogs/sendto/Services/ObexAgent.vala +++ b/src/dialogs/sendto/Services/ObexAgent.vala @@ -20,10 +20,10 @@ public class Bluetooth.Obex.Agent : GLib.Object { /* one confirmation for many files in one session */ private GLib.ObjectPath many_files; - public signal void response_notify(string address, GLib.ObjectPath objectpath); - public signal void response_accepted(string address, GLib.ObjectPath objectpath); + public signal void response_notify(string address, ObjectPath object_path); + public signal void response_accepted(string address, ObjectPath object_path); public signal void transfer_view(string session_path); - public signal void response_canceled(); + public signal void response_canceled(ObjectPath? object_path = null); public Agent() { Bus.own_name( @@ -76,7 +76,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { Bluetooth.Obex.Transfer transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", object_path); if (transfer.name == null) { - throw new BluezObexError.REJECTED("Authorize Reject"); + throw new BluezObexError.REJECTED("File transfer rejected"); } Bluetooth.Obex.Session session = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", transfer.session); @@ -95,8 +95,8 @@ public class Bluetooth.Obex.Agent : GLib.Object { var cancel_action = new SimpleAction("btcancel", VariantType.STRING); GLib.Application.get_default().add_action(cancel_action); cancel_action.activate.connect((parameter) => { - obex_error = new BluezObexError.CANCELED("Authorize Cancel"); - response_canceled(); + obex_error = new BluezObexError.CANCELED("File transfer cancelled"); + response_canceled(object_path); if (callback != null) { Idle.add((owned) callback); } From 8f0a34a049acef32fe7b9870385c20f29375f443 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Thu, 24 Aug 2023 22:25:39 -0400 Subject: [PATCH 67/81] bluetooth-indicator: Use symbolic icons for the tray item Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 331b137a0..d6d7d7cf8 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -314,11 +314,11 @@ public class BluetoothIndicator : Bin { // Update the tray icon and placeholder text if (enabled) { // Airplane mode is on, so Bluetooth is disabled - image.set_from_icon_name("bluetooth-disabled", IconSize.MENU); + image.set_from_icon_name("bluetooth-disabled-symbolic", IconSize.MENU); placeholder_label.label = _("Airplane mode is on."); placeholder_sublabel.label = _("Bluetooth is disabled while airplane mode is on."); } else { // Airplane mode is off, so Bluetooth is enabled - image.set_from_icon_name("bluetooth-active", IconSize.MENU); + image.set_from_icon_name("bluetooth-active-symbolic", IconSize.MENU); placeholder_label.label = _("No paired Bluetooth devices found."); placeholder_sublabel.label = _("Visit Bluetooth settings to pair a device."); } From 419cae4467c2aeba059f10ecad80175c618625d6 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 26 Aug 2023 11:59:00 -0400 Subject: [PATCH 68/81] sendto: Filter devices in the device chooser by connected state Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/DeviceRow.vala | 23 +------------- src/dialogs/sendto/Dialog/ScanDialog.vala | 38 +++++++++++++++++++++-- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/dialogs/sendto/Dialog/DeviceRow.vala b/src/dialogs/sendto/Dialog/DeviceRow.vala index b341d70fa..43ed20330 100644 --- a/src/dialogs/sendto/Dialog/DeviceRow.vala +++ b/src/dialogs/sendto/Dialog/DeviceRow.vala @@ -16,7 +16,6 @@ public class DeviceRow : Gtk.ListBoxRow { private static Gtk.SizeGroup size_group; private Gtk.Button send_button; - private Gtk.Image state_image; private Gtk.Label state_label; public signal void send_clicked(Bluetooth.Device device); @@ -32,21 +31,11 @@ public class DeviceRow : Gtk.ListBoxRow { construct { var image = new Gtk.Image.from_icon_name(device.icon ?? "bluetooth-active", Gtk.IconSize.DND); - state_image = new Gtk.Image.from_icon_name("user-offline", Gtk.IconSize.MENU) { - valign = Gtk.Align.END, - halign = Gtk.Align.END, - }; - state_label = new Gtk.Label(null) { use_markup = true, xalign = 0, }; - var overlay = new Gtk.Overlay(); - overlay.tooltip_text = device.address; - overlay.add(image); - overlay.add_overlay(state_image); - string? device_name = device.alias; if (device_name == null) { if (device.icon == null) { @@ -75,7 +64,7 @@ public class DeviceRow : Gtk.ListBoxRow { orientation = Gtk.Orientation.HORIZONTAL, }; - grid.attach(overlay, 0, 0, 1, 2); + grid.attach(image, 0, 0, 1, 2); grid.attach(label, 1, 0, 1, 1); grid.attach(state_label, 1, 1, 1, 1); grid.attach(send_button, 4, 0, 1, 2); @@ -85,7 +74,6 @@ public class DeviceRow : Gtk.ListBoxRow { show_all(); set_sensitive(adapter.powered); - set_status(device.connected); ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { var powered = changed.lookup_value("Powered", new VariantType("b")); @@ -95,11 +83,6 @@ public class DeviceRow : Gtk.ListBoxRow { }); ((DBusProxy) device).g_properties_changed.connect((changed, invalid) => { - var connected = changed.lookup_value("Connected", new VariantType("b")); - if (connected != null) { - set_status(device.connected); - } - var name = changed.lookup_value("Name", new VariantType("s")); if (name != null) { label.label = device.alias; @@ -140,8 +123,4 @@ public class DeviceRow : Gtk.ListBoxRow { return device.address; } } - - private void set_status(bool status) { - state_image.icon_name = status ? "user-available" : "user-offline"; - } } diff --git a/src/dialogs/sendto/Dialog/ScanDialog.vala b/src/dialogs/sendto/Dialog/ScanDialog.vala index b37aa1107..9b2f6dd2c 100644 --- a/src/dialogs/sendto/Dialog/ScanDialog.vala +++ b/src/dialogs/sendto/Dialog/ScanDialog.vala @@ -41,7 +41,9 @@ public class ScanDialog : Gtk.Dialog { xalign = 0, }; - var placeholder = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + var placeholder = new Gtk.Box(Gtk.Orientation.VERTICAL, 18) { + margin_top = 125, + }; var placeholder_title = new Gtk.Label(_("No devices found")) { use_markup = true, }; @@ -49,8 +51,8 @@ public class ScanDialog : Gtk.Dialog { var placeholder_text = new Gtk.Label(_("Ensure that your devices are visable and ready for pairing")); placeholder_text.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); - placeholder.pack_start(placeholder_title); - placeholder.pack_start(placeholder_text); + placeholder.pack_start(placeholder_title, false); + placeholder.pack_start(placeholder_text, false); placeholder.show_all(); devices_box = new Gtk.ListBox() { @@ -60,6 +62,7 @@ public class ScanDialog : Gtk.Dialog { devices_box.set_header_func((Gtk.ListBoxUpdateHeaderFunc) title_rows); devices_box.set_sort_func((Gtk.ListBoxSortFunc) compare_rows); + devices_box.set_filter_func((Gtk.ListBoxFilterFunc) filter_row); devices_box.set_placeholder(placeholder); var scrolled_window = new Gtk.ScrolledWindow(null, null) { @@ -139,6 +142,20 @@ public class ScanDialog : Gtk.Dialog { manager.stop_discovery.begin(); send_file(device); }); + + ((DBusProxy) row.device).g_properties_changed.connect((changed, invalid) => { + var paired = changed.lookup_value("Paired", new VariantType("b")); + if (paired != null) { + invalidate_filters(); + } + + var connected = changed.lookup_value("Connected", new VariantType("b")); + if (connected != null) { + invalidate_filters(); + } + }); + + invalidate_filters(); } private void device_removed(Bluetooth.Device device) { @@ -148,6 +165,14 @@ public class ScanDialog : Gtk.Dialog { break; } } + + invalidate_filters(); + } + + private void invalidate_filters() { + devices_box.invalidate_filter(); + devices_box.invalidate_headers(); + devices_box.invalidate_sort(); } [CCode (instance_pos = -1)] @@ -198,4 +223,11 @@ public class ScanDialog : Gtk.Dialog { row1.set_header(null); } } + + [CCode (instance_pos = -1)] + private bool filter_row(DeviceRow row) { + unowned Bluetooth.Device device = row.device; + + return device.paired && device.connected; + } } From 54cddb8b6aab7e75f76be85f728e1139a043a356 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 26 Aug 2023 12:33:56 -0400 Subject: [PATCH 69/81] sendto: Let's actually use the grid we're creating for the header Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/ScanDialog.vala | 46 ++++++++++++++++++----- src/dialogs/sendto/Services/Manager.vala | 4 +- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/dialogs/sendto/Dialog/ScanDialog.vala b/src/dialogs/sendto/Dialog/ScanDialog.vala index 9b2f6dd2c..e7eadbc71 100644 --- a/src/dialogs/sendto/Dialog/ScanDialog.vala +++ b/src/dialogs/sendto/Dialog/ScanDialog.vala @@ -12,6 +12,8 @@ public class ScanDialog : Gtk.Dialog { public Bluetooth.ObjectManager manager { get; construct; } + private Gtk.Revealer status_revealer; + private Gtk.Spinner spinner; private Gtk.ListBox devices_box; public signal void send_file(Bluetooth.Device device); @@ -21,26 +23,41 @@ public class ScanDialog : Gtk.Dialog { } construct { + title = _("Bluetooth File Transfer"); + var icon_image = new Gtk.Image.from_icon_name("bluetooth-active", Gtk.IconSize.DIALOG) { valign = Gtk.Align.CENTER, halign = Gtk.Align.CENTER, }; - var title_label = new Gtk.Label(_("Bluetooth File Transfer")) { + var info_label = new Gtk.Label(_("Select a Bluetooth device to send files to")) { max_width_chars = 45, use_markup = true, wrap = true, xalign = 0, }; - title_label.get_style_context().add_class("primary"); - var info_label = new Gtk.Label(_("Select a Bluetooth device to send files to")) { + status_revealer = new Gtk.Revealer() { + reveal_child = false, + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + }; + + spinner = new Gtk.Spinner() { + margin = 4, + }; + + var status_label = new Gtk.Label(_("Discovering…")) { max_width_chars = 45, - use_markup = true, wrap = true, xalign = 0, }; + var status_grid = new Gtk.Grid(); + status_grid.attach(spinner, 0, 0, 1, 1); + status_grid.attach(status_label, 1, 0, 1, 1); + + status_revealer.add(status_grid); + var placeholder = new Gtk.Box(Gtk.Orientation.VERTICAL, 18) { margin_top = 125, }; @@ -71,12 +88,13 @@ public class ScanDialog : Gtk.Dialog { scrolled_window.add(devices_box); var grid = new Gtk.Grid() { + margin_top = 10, margin_bottom = 10, }; grid.attach(icon_image, 0, 0, 1, 2); - grid.attach(title_label, 1, 0, 1, 1); - grid.attach(info_label, 1, 1, 1, 1); + grid.attach(info_label, 1, 0, 1, 1); + grid.attach(status_revealer, 1, 1, 1, 1); var devices_grid = new Gtk.Grid() { orientation = Gtk.Orientation.VERTICAL, @@ -90,6 +108,7 @@ public class ScanDialog : Gtk.Dialog { // devices_grid.add(frame); devices_grid.add(scrolled_window); + get_content_area().add(grid); get_content_area().add(devices_grid); add_button(_("Close"), Gtk.ResponseType.CLOSE); @@ -101,9 +120,7 @@ public class ScanDialog : Gtk.Dialog { // Connect manager signals manager.device_added.connect(add_device); manager.device_removed.connect(device_removed); - manager.status_discovering.connect(() => { - // TODO: dunno how or if to show this yet - }); + manager.status_discovering.connect(update_status); } public override void show() { @@ -115,6 +132,17 @@ public class ScanDialog : Gtk.Dialog { } manager.start_discovery.begin(); + update_status(); + } + + private void update_status() { + if (manager.check_discovering()) { + spinner.start(); + status_revealer.set_reveal_child(true); + } else { + spinner.stop(); + status_revealer.set_reveal_child(false); + } } private void add_device(Bluetooth.Device device) { diff --git a/src/dialogs/sendto/Services/Manager.vala b/src/dialogs/sendto/Services/Manager.vala index aeb4b5473..fd3d3e848 100644 --- a/src/dialogs/sendto/Services/Manager.vala +++ b/src/dialogs/sendto/Services/Manager.vala @@ -158,7 +158,9 @@ public class Bluetooth.ObjectManager : Object { var adapters = get_adapters(); foreach (var adapter in adapters) { - return adapter.discovering; + if (adapter.discovering) { + return true; + } } return false; From 7c40c6e1130aa2f27d339e376a9e4a14673045c8 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 26 Aug 2023 12:42:52 -0400 Subject: [PATCH 70/81] sendto: Somehow these files were using spaces instead of tabs Signed-off-by: Evan Maddock --- src/dialogs/sendto/Services/Adapter.vala | 30 +- src/dialogs/sendto/Services/Manager.vala | 342 ++++++++++----------- src/dialogs/sendto/Services/ObexAgent.vala | 130 ++++---- src/dialogs/sendto/Services/Session.vala | 8 +- src/dialogs/sendto/Services/Transfer.vala | 20 +- 5 files changed, 265 insertions(+), 265 deletions(-) diff --git a/src/dialogs/sendto/Services/Adapter.vala b/src/dialogs/sendto/Services/Adapter.vala index 142b13b31..c9d3722d8 100644 --- a/src/dialogs/sendto/Services/Adapter.vala +++ b/src/dialogs/sendto/Services/Adapter.vala @@ -12,20 +12,20 @@ [DBus (name = "org.bluez.Adapter1")] public interface Bluetooth.Adapter : Object { public abstract string[] UUIDs { owned get; } - public abstract bool discoverable { get; set; } - public abstract bool discovering { get; } - public abstract bool pairable { get; set; } - public abstract bool powered { get; set; } - public abstract string address { owned get; } - public abstract string alias { owned get; set; } - public abstract string modalias { owned get; } - public abstract string name { owned get; } - public abstract uint @class { get; } - public abstract uint discoverable_timeout { get; } - public abstract uint pairable_timeout { get; } + public abstract bool discoverable { get; set; } + public abstract bool discovering { get; } + public abstract bool pairable { get; set; } + public abstract bool powered { get; set; } + public abstract string address { owned get; } + public abstract string alias { owned get; set; } + public abstract string modalias { owned get; } + public abstract string name { owned get; } + public abstract uint @class { get; } + public abstract uint discoverable_timeout { get; } + public abstract uint pairable_timeout { get; } - public abstract void remove_device(ObjectPath device) throws Error; - public abstract void set_discovery_filter(HashTable properties) throws Error; - public abstract async void start_discovery() throws Error; - public abstract async void stop_discovery() throws Error; + public abstract void remove_device(ObjectPath device) throws Error; + public abstract void set_discovery_filter(HashTable properties) throws Error; + public abstract async void start_discovery() throws Error; + public abstract async void stop_discovery() throws Error; } diff --git a/src/dialogs/sendto/Services/Manager.vala b/src/dialogs/sendto/Services/Manager.vala index fd3d3e848..8b12102a4 100644 --- a/src/dialogs/sendto/Services/Manager.vala +++ b/src/dialogs/sendto/Services/Manager.vala @@ -12,72 +12,72 @@ public class Bluetooth.ObjectManager : Object { public bool has_object { get; private set; default = false; } - private GLib.DBusObjectManagerClient object_manager; - - public signal void device_added(Bluetooth.Device device); - public signal void device_removed(Bluetooth.Device device); - public signal void status_discovering(); - - construct { - create_manager.begin(); - register_obex_agentmanager(); - } - - public async void create_manager() { - try { - object_manager = yield new GLib.DBusObjectManagerClient.for_bus.begin( - BusType.SYSTEM, - GLib.DBusObjectManagerClientFlags.NONE, - "org.bluez", - "/", - object_manager_proxy_get_type, - null - ); - - object_manager.get_objects().foreach((object) => { - object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); - }); - - object_manager.interface_added.connect(on_interface_added); - - object_manager.interface_removed.connect(on_interface_removed); - - object_manager.object_added.connect((object) => { - object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); - }); - - object_manager.object_removed.connect((object) => { - object.get_interfaces().foreach((iface) => on_interface_removed(object, iface)); - }); - } catch (Error e) { - critical("Error getting Bluez object manager: %s", e.message); - } - } - - //TODO: Do not rely on this when it is possible to do it natively in Vala - [CCode (cname="bluetooth_device_proxy_get_type")] - extern static GLib.Type get_device_proxy_type(); - - [CCode (cname="bluetooth_adapter_proxy_get_type")] - extern static GLib.Type get_adapter_proxy_type(); - - private GLib.Type object_manager_proxy_get_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 void register_obex_agentmanager() { - try { - var connection = GLib.Bus.get_sync(BusType.SESSION); - connection.call.begin( + private GLib.DBusObjectManagerClient object_manager; + + public signal void device_added(Bluetooth.Device device); + public signal void device_removed(Bluetooth.Device device); + public signal void status_discovering(); + + construct { + create_manager.begin(); + register_obex_agentmanager(); + } + + public async void create_manager() { + try { + object_manager = yield new GLib.DBusObjectManagerClient.for_bus.begin( + BusType.SYSTEM, + GLib.DBusObjectManagerClientFlags.NONE, + "org.bluez", + "/", + object_manager_proxy_get_type, + null + ); + + object_manager.get_objects().foreach((object) => { + object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); + }); + + object_manager.interface_added.connect(on_interface_added); + + object_manager.interface_removed.connect(on_interface_removed); + + object_manager.object_added.connect((object) => { + object.get_interfaces().foreach((iface) => on_interface_added(object, iface)); + }); + + object_manager.object_removed.connect((object) => { + object.get_interfaces().foreach((iface) => on_interface_removed(object, iface)); + }); + } catch (Error e) { + critical("Error getting Bluez object manager: %s", e.message); + } + } + + //TODO: Do not rely on this when it is possible to do it natively in Vala + [CCode (cname="bluetooth_device_proxy_get_type")] + extern static GLib.Type get_device_proxy_type(); + + [CCode (cname="bluetooth_adapter_proxy_get_type")] + extern static GLib.Type get_adapter_proxy_type(); + + private GLib.Type object_manager_proxy_get_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 void register_obex_agentmanager() { + try { + var connection = GLib.Bus.get_sync(BusType.SESSION); + connection.call.begin( "org.bluez.obex", "/org/bluez/obex", "org.bluez.obex.AgentManager1", @@ -86,121 +86,121 @@ public class Bluetooth.ObjectManager : Object { null, GLib.DBusCallFlags.NONE, -1); - } catch (Error e) { - critical("Error registering Obex agent manager: %s", e.message); - } - } - - private void on_interface_added(GLib.DBusObject object, GLib.DBusInterface iface) { - if (iface is Bluetooth.Device) { - unowned var device = (Bluetooth.Device) iface; - device_added(device); - } else if (iface is Bluetooth.Adapter) { - unowned var adapter = (Bluetooth.Adapter) iface; - has_object = true; - ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { - var discovering = changed.lookup_value("Discovering", GLib.VariantType.BOOLEAN); - if (discovering != null) { - status_discovering(); - } - }); - } - } - - private void on_interface_removed(GLib.DBusObject object, GLib.DBusInterface iface) { - if (iface is Bluetooth.Device) { - device_removed((Bluetooth.Device) iface); - } else if (iface is Bluetooth.Adapter) { - has_object = !get_adapters().is_empty; - } - } - - public Gee.LinkedList get_adapters() requires (object_manager != null) { - var adapters = new Gee.LinkedList(); - - object_manager.get_objects().foreach((object) => { - GLib.DBusInterface? iface = object.get_interface("org.bluez.Adapter1"); - if (iface == null) return; - - adapters.add(((Bluetooth.Adapter) iface)); - }); - - return (owned) adapters; - } - - public Gee.Collection get_devices() requires (object_manager != null) { - var devices = new Gee.LinkedList(); - - object_manager.get_objects().foreach((object) => { - GLib.DBusInterface? iface = object.get_interface("org.bluez.Device1"); - if (iface == null) return; - - devices.add(((Bluetooth.Device) iface)); - }); - - return (owned) devices; - } - - public async void start_discovery() { - var adapters = get_adapters(); - - foreach (var adapter in adapters) { - try { - adapter.discoverable = true; - yield adapter.start_discovery(); - } catch (Error e) { - critical("Error starting discovery on Bluetooth adapter '%s': %s", adapter.name, e.message); - } - } - } - - public bool check_discovering() { - var adapters = get_adapters(); - - foreach (var adapter in adapters) { - if (adapter.discovering) { + } catch (Error e) { + critical("Error registering Obex agent manager: %s", e.message); + } + } + + private void on_interface_added(GLib.DBusObject object, GLib.DBusInterface iface) { + if (iface is Bluetooth.Device) { + unowned var device = (Bluetooth.Device) iface; + device_added(device); + } else if (iface is Bluetooth.Adapter) { + unowned var adapter = (Bluetooth.Adapter) iface; + has_object = true; + ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { + var discovering = changed.lookup_value("Discovering", GLib.VariantType.BOOLEAN); + if (discovering != null) { + status_discovering(); + } + }); + } + } + + private void on_interface_removed(GLib.DBusObject object, GLib.DBusInterface iface) { + if (iface is Bluetooth.Device) { + device_removed((Bluetooth.Device) iface); + } else if (iface is Bluetooth.Adapter) { + has_object = !get_adapters().is_empty; + } + } + + public Gee.LinkedList get_adapters() requires (object_manager != null) { + var adapters = new Gee.LinkedList(); + + object_manager.get_objects().foreach((object) => { + GLib.DBusInterface? iface = object.get_interface("org.bluez.Adapter1"); + if (iface == null) return; + + adapters.add(((Bluetooth.Adapter) iface)); + }); + + return (owned) adapters; + } + + public Gee.Collection get_devices() requires (object_manager != null) { + var devices = new Gee.LinkedList(); + + object_manager.get_objects().foreach((object) => { + GLib.DBusInterface? iface = object.get_interface("org.bluez.Device1"); + if (iface == null) return; + + devices.add(((Bluetooth.Device) iface)); + }); + + return (owned) devices; + } + + public async void start_discovery() { + var adapters = get_adapters(); + + foreach (var adapter in adapters) { + try { + adapter.discoverable = true; + yield adapter.start_discovery(); + } catch (Error e) { + critical("Error starting discovery on Bluetooth adapter '%s': %s", adapter.name, e.message); + } + } + } + + public bool check_discovering() { + var adapters = get_adapters(); + + foreach (var adapter in adapters) { + if (adapter.discovering) { return true; } - } + } - return false; - } + return false; + } - public async void stop_discovery() { - var adapters = get_adapters(); + public async void stop_discovery() { + var adapters = get_adapters(); - foreach (var adapter in adapters) { - adapter.discoverable = false; + foreach (var adapter in adapters) { + adapter.discoverable = false; - try { - if (adapter.powered && adapter.discovering) { - yield adapter.stop_discovery(); - } - } catch (Error e) { - critical("Error stopping discovery on Bluetooth adapter '%s': %s", adapter.name, e.message); - } - } - } + try { + if (adapter.powered && adapter.discovering) { + yield adapter.stop_discovery(); + } + } catch (Error e) { + critical("Error stopping discovery on Bluetooth adapter '%s': %s", adapter.name, e.message); + } + } + } - public Bluetooth.Adapter? get_adapter_from_path(string path) { - GLib.DBusObject? object = object_manager.get_object(path); + public Bluetooth.Adapter? get_adapter_from_path(string path) { + GLib.DBusObject? object = object_manager.get_object(path); - if (object != null) { - return (Bluetooth.Adapter?) object.get_interface("org.bluez.Adapter1"); - } + if (object != null) { + return (Bluetooth.Adapter?) object.get_interface("org.bluez.Adapter1"); + } - return null; - } + return null; + } - public Bluetooth.Device? get_device(string address) { - var devices = get_devices(); + public Bluetooth.Device? get_device(string address) { + var devices = get_devices(); - foreach (var device in devices) { - if (device.address == address) { - return device; - } - } + foreach (var device in devices) { + if (device.address == address) { + return device; + } + } - return null; - } + return null; + } } diff --git a/src/dialogs/sendto/Services/ObexAgent.vala b/src/dialogs/sendto/Services/ObexAgent.vala index e37ca2da7..58f42c245 100644 --- a/src/dialogs/sendto/Services/ObexAgent.vala +++ b/src/dialogs/sendto/Services/ObexAgent.vala @@ -11,28 +11,28 @@ [DBus (name = "org.bluez.obex.Error")] public errordomain BluezObexError { - REJECTED, - CANCELED + REJECTED, + CANCELED } [DBus (name = "org.bluez.obex.Agent1")] public class Bluetooth.Obex.Agent : GLib.Object { /* one confirmation for many files in one session */ - private GLib.ObjectPath many_files; - - public signal void response_notify(string address, ObjectPath object_path); - public signal void response_accepted(string address, ObjectPath object_path); - public signal void transfer_view(string session_path); - public signal void response_canceled(ObjectPath? object_path = null); - - public Agent() { - Bus.own_name( - BusType.SESSION, - "org.bluez.obex.Agent1", - GLib.BusNameOwnerFlags.NONE, - on_name_get - ); - } + private GLib.ObjectPath many_files; + + public signal void response_notify(string address, ObjectPath object_path); + public signal void response_accepted(string address, ObjectPath object_path); + public signal void transfer_view(string session_path); + public signal void response_canceled(ObjectPath? object_path = null); + + public Agent() { + Bus.own_name( + BusType.SESSION, + "org.bluez.obex.Agent1", + GLib.BusNameOwnerFlags.NONE, + on_name_get + ); + } private void on_name_get(GLib.DBusConnection conn) { try { @@ -42,9 +42,9 @@ public class Bluetooth.Obex.Agent : GLib.Object { } } - public void transfer_active(string session_path) throws GLib.Error { - transfer_view(session_path); - } + public void transfer_active(string session_path) throws GLib.Error { + transfer_view(session_path); + } /** * release: @@ -55,7 +55,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { * agent, because when this method gets called it has * already been unregistered. */ - public void release() throws GLib.Error {} + public void release() throws GLib.Error {} /** * authorize_push: @@ -70,60 +70,60 @@ public class Bluetooth.Obex.Agent : GLib.Object { * property that contains the default location and name * that can be returned. */ - public async string authorize_push(GLib.ObjectPath object_path) throws Error { - SourceFunc callback = authorize_push.callback; - BluezObexError? obex_error = null; - Bluetooth.Obex.Transfer transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", object_path); + public async string authorize_push(GLib.ObjectPath object_path) throws Error { + SourceFunc callback = authorize_push.callback; + BluezObexError? obex_error = null; + Bluetooth.Obex.Transfer transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", object_path); - if (transfer.name == null) { - throw new BluezObexError.REJECTED("File transfer rejected"); - } + if (transfer.name == null) { + throw new BluezObexError.REJECTED("File transfer rejected"); + } - Bluetooth.Obex.Session session = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", transfer.session); + Bluetooth.Obex.Session session = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", transfer.session); // Register application action to accept a file transfer - var accept_action = new SimpleAction("btaccept", VariantType.STRING); - GLib.Application.get_default().add_action(accept_action); - accept_action.activate.connect((parameter) => { - response_accepted(session.destination, object_path); - if (callback != null) { - Idle.add((owned) callback); - } - }); + var accept_action = new SimpleAction("btaccept", VariantType.STRING); + GLib.Application.get_default().add_action(accept_action); + accept_action.activate.connect((parameter) => { + response_accepted(session.destination, object_path); + if (callback != null) { + Idle.add((owned) callback); + } + }); // Register application action to reject a file transfer - var cancel_action = new SimpleAction("btcancel", VariantType.STRING); - GLib.Application.get_default().add_action(cancel_action); - cancel_action.activate.connect((parameter) => { - obex_error = new BluezObexError.CANCELED("File transfer cancelled"); - response_canceled(object_path); - if (callback != null) { - Idle.add((owned) callback); - } - }); + var cancel_action = new SimpleAction("btcancel", VariantType.STRING); + GLib.Application.get_default().add_action(cancel_action); + cancel_action.activate.connect((parameter) => { + obex_error = new BluezObexError.CANCELED("File transfer cancelled"); + response_canceled(object_path); + if (callback != null) { + Idle.add((owned) callback); + } + }); // Automatically accept the transfer if there are multiple files for // the one transfer - if (many_files == object_path) { - Idle.add(()=>{ - response_accepted(session.destination, object_path); - if (callback != null) { - Idle.add((owned) callback); - } - return GLib.Source.REMOVE; - }); - } else { + if (many_files == object_path) { + Idle.add(()=>{ + response_accepted(session.destination, object_path); + if (callback != null) { + Idle.add((owned) callback); + } + return GLib.Source.REMOVE; + }); + } else { // Not multple files, ask to accept or reject - response_notify(session.destination, object_path); - } + response_notify(session.destination, object_path); + } - yield; + yield; - if (obex_error != null) throw obex_error; + if (obex_error != null) throw obex_error; - many_files = object_path; - return transfer.name; - } + many_files = object_path; + return transfer.name; + } /** * cancel: @@ -132,7 +132,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { * request failed before a reply was returned. It cancels * the previous request. */ - public void cancel() throws GLib.Error { - response_canceled(); - } + public void cancel() throws GLib.Error { + response_canceled(); + } } diff --git a/src/dialogs/sendto/Services/Session.vala b/src/dialogs/sendto/Services/Session.vala index 50d8a1a94..d2982d4bd 100644 --- a/src/dialogs/sendto/Services/Session.vala +++ b/src/dialogs/sendto/Services/Session.vala @@ -12,10 +12,10 @@ [DBus (name = "org.bluez.obex.Session1")] public interface Bluetooth.Obex.Session : Object { public abstract string source { owned get; } - public abstract string destination { owned get; } - public abstract uchar channel { owned get; } - public abstract string target { owned get; } - public abstract string root { owned get; } + public abstract string destination { owned get; } + public abstract uchar channel { owned get; } + public abstract string target { owned get; } + public abstract string root { owned get; } public abstract string get_capabilities() throws GLib.Error; } diff --git a/src/dialogs/sendto/Services/Transfer.vala b/src/dialogs/sendto/Services/Transfer.vala index d897e6e6c..debff641d 100644 --- a/src/dialogs/sendto/Services/Transfer.vala +++ b/src/dialogs/sendto/Services/Transfer.vala @@ -12,15 +12,15 @@ [DBus (name = "org.bluez.obex.Transfer1")] public interface Bluetooth.Obex.Transfer : Object { public abstract string status { owned get; } - public abstract ObjectPath session { owned get; } - public abstract string name { owned get; } - public abstract string Type { owned get; } - public abstract uint64 time { owned get; } - public abstract uint64 size { owned get; } - public abstract uint64 transferred { owned get; } - public abstract string filename { owned get; } + public abstract ObjectPath session { owned get; } + public abstract string name { owned get; } + public abstract string Type { owned get; } + public abstract uint64 time { owned get; } + public abstract uint64 size { owned get; } + public abstract uint64 transferred { owned get; } + public abstract string filename { owned get; } - public abstract void cancel() throws GLib.Error; - public abstract void resume() throws GLib.Error; - public abstract void suspend() throws GLib.Error; + public abstract void cancel() throws GLib.Error; + public abstract void resume() throws GLib.Error; + public abstract void suspend() throws GLib.Error; } From b9a67b568987e17dfc862aaffbfe9663f6142e8a Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Sat, 26 Aug 2023 12:48:15 -0400 Subject: [PATCH 71/81] sendto: Add titles and margins to send and receive dialogs Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/FileReceiver.vala | 5 +++-- src/dialogs/sendto/Dialog/FileSender.vala | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala index a0377f779..e05d3cfe3 100644 --- a/src/dialogs/sendto/Dialog/FileReceiver.vala +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -34,6 +34,8 @@ public class FileReceiver : Gtk.Dialog { } construct { + title = _("Bluetooth File Transfer"); + notification = new Notification("Bluetooth"); notification.set_priority(NotificationPriority.NORMAL); @@ -100,8 +102,7 @@ public class FileReceiver : Gtk.Dialog { column_spacing = 0, row_spacing = 4, width_request = 450, - margin_start = 10, - margin_end = 15 + margin = 10, }; message_grid.attach(overlay, 0, 0, 1, 3); diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala index 992bfb864..edf305dbd 100644 --- a/src/dialogs/sendto/Dialog/FileSender.vala +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -39,6 +39,8 @@ public class FileSender : Gtk.Dialog { } construct { + title = _("Bluetooth File Transfer"); + file_store = new Gtk.ListStore(1, typeof(GLib.File)); var icon_image = new Gtk.Image.from_icon_name ("bluetooth-active", Gtk.IconSize.DIALOG) { @@ -104,8 +106,7 @@ public class FileSender : Gtk.Dialog { column_spacing = 0, row_spacing = 4, width_request = 450, - margin_start = 10, - margin_end = 15 + margin = 10, }; message_grid.attach(overlay, 0, 0, 1, 3); From d022e6bcc28e1b240da160d5a89d5f0121ab86c6 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Thu, 14 Sep 2023 13:30:50 -0400 Subject: [PATCH 72/81] sendto: Cleanup Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/DeviceRow.vala | 4 +++- src/dialogs/sendto/Dialog/FileReceiver.vala | 7 +++--- src/dialogs/sendto/Dialog/FileSender.vala | 15 ++++++------ src/dialogs/sendto/Services/Manager.vala | 26 ++++++++++----------- src/dialogs/sendto/Services/ObexAgent.vala | 18 +++++++------- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/dialogs/sendto/Dialog/DeviceRow.vala b/src/dialogs/sendto/Dialog/DeviceRow.vala index 43ed20330..4fa6f6aac 100644 --- a/src/dialogs/sendto/Dialog/DeviceRow.vala +++ b/src/dialogs/sendto/Dialog/DeviceRow.vala @@ -38,7 +38,9 @@ public class DeviceRow : Gtk.ListBoxRow { string? device_name = device.alias; if (device_name == null) { - if (device.icon == null) { + if (device.name != null) { + device_name = device.name; + } else if (device.icon != null) { device_name = get_name_from_icon(); } else { device_name = device.address; diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala index e05d3cfe3..a8887e935 100644 --- a/src/dialogs/sendto/Dialog/FileReceiver.vala +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -39,7 +39,7 @@ public class FileReceiver : Gtk.Dialog { notification = new Notification("Bluetooth"); notification.set_priority(NotificationPriority.NORMAL); - var icon_image = new Gtk.Image.from_icon_name ("bluetooth-active", Gtk.IconSize.DIALOG) { + var icon_image = new Gtk.Image.from_icon_name("bluetooth-active", Gtk.IconSize.DIALOG) { valign = Gtk.Align.END, halign = Gtk.Align.END, }; @@ -61,7 +61,6 @@ public class FileReceiver : Gtk.Dialog { xalign = 0, use_markup = true, }; - device_label.get_style_context().add_class("primary"); directory_label = new Gtk.Label(null) { max_width_chars = 45, @@ -162,7 +161,7 @@ public class FileReceiver : Gtk.Dialog { total_size = transfer.size; session_path = transfer.session; - filename_label.set_markup(Markup.printf_escaped (_("File name: %s"), transfer.name)); + filename_label.set_markup(Markup.printf_escaped(_("File name: %s"), transfer.name)); } catch (Error e) { warning("Error accepting Bluetooth file transfer: %s", e.message); } @@ -234,10 +233,12 @@ public class FileReceiver : Gtk.Dialog { progress_bar.fraction = (double) transferred / (double) total_size; int current_time = (int) get_real_time(); int elapsed_time = (current_time - start_time) / 1000000; + if (current_time < start_time + 1000000) return; if (elapsed_time == 0) return; uint64 transfer_rate = transferred / elapsed_time; + if (transfer_rate == 0) return; rate_label.label = Markup.printf_escaped(_("Transfer rate: %s / s"), format_size(transfer_rate)); diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala index edf305dbd..cbe4b6cca 100644 --- a/src/dialogs/sendto/Dialog/FileSender.vala +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -43,7 +43,7 @@ public class FileSender : Gtk.Dialog { file_store = new Gtk.ListStore(1, typeof(GLib.File)); - var icon_image = new Gtk.Image.from_icon_name ("bluetooth-active", Gtk.IconSize.DIALOG) { + var icon_image = new Gtk.Image.from_icon_name("bluetooth-active", Gtk.IconSize.DIALOG) { valign = Gtk.Align.END, halign = Gtk.Align.END, }; @@ -65,7 +65,6 @@ public class FileSender : Gtk.Dialog { xalign = 0, use_markup = true, }; - path_label.get_style_context().add_class("primary"); device_label = new Gtk.Label(Markup.printf_escaped("%s:", _("To"))) { max_width_chars = 45, @@ -224,9 +223,9 @@ public class FileSender : Gtk.Dialog { variant_client.get("(o)", out session_path); // Create our Obex session - session = yield new GLib.DBusProxy ( + session = yield new GLib.DBusProxy( connection, - GLib.DBusProxyFlags.NONE, + DBusProxyFlags.NONE, null, "org.bluez.obex", session_path, @@ -255,7 +254,7 @@ public class FileSender : Gtk.Dialog { }; retry_dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL); - var suggested_button = retry_dialog.add_button(_("Accept"), Gtk.ResponseType.ACCEPT); + var suggested_button = retry_dialog.add_button(_("Retry"), Gtk.ResponseType.ACCEPT); suggested_button.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); retry_dialog.response.connect((response_id) => { @@ -287,7 +286,7 @@ public class FileSender : Gtk.Dialog { // Update the labels path_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), file_path.get_parent().get_path())); device_label.set_markup(GLib.Markup.printf_escaped(_("To: %s"), device.alias)); - icon_label.set_from_gicon(new ThemedIcon(device.icon == null ? "bluetooth-active" : device.icon), Gtk.IconSize.LARGE_TOOLBAR); + icon_label.set_from_gicon(new ThemedIcon(device.icon ?? "bluetooth-active"), Gtk.IconSize.LARGE_TOOLBAR); progress_label.label = _("Waiting for acceptance on %s…").printf(device.alias); try { @@ -336,7 +335,7 @@ public class FileSender : Gtk.Dialog { }; retry_dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL); - var suggested_button = retry_dialog.add_button(_("Accept"), Gtk.ResponseType.ACCEPT); + var suggested_button = retry_dialog.add_button(_("Retry"), Gtk.ResponseType.ACCEPT); suggested_button.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); retry_dialog.response.connect((response_id) => { @@ -421,7 +420,7 @@ public class FileSender : Gtk.Dialog { private void send_notify() { var notification = new Notification("Bluetooth"); - notification.set_icon(new ThemedIcon(device.icon)); + notification.set_icon(new ThemedIcon(device.icon ?? "bluetooth-active")); notification.set_title(_("File transferred successfully")); notification.set_body(Markup.printf_escaped("From: %s Sent to: %s", file_path.get_path(), device.alias)); notification.set_priority(NotificationPriority.NORMAL); diff --git a/src/dialogs/sendto/Services/Manager.vala b/src/dialogs/sendto/Services/Manager.vala index 8b12102a4..0d9aa9e75 100644 --- a/src/dialogs/sendto/Services/Manager.vala +++ b/src/dialogs/sendto/Services/Manager.vala @@ -12,7 +12,7 @@ public class Bluetooth.ObjectManager : Object { public bool has_object { get; private set; default = false; } - private GLib.DBusObjectManagerClient object_manager; + private DBusObjectManagerClient object_manager; public signal void device_added(Bluetooth.Device device); public signal void device_removed(Bluetooth.Device device); @@ -25,9 +25,9 @@ public class Bluetooth.ObjectManager : Object { public async void create_manager() { try { - object_manager = yield new GLib.DBusObjectManagerClient.for_bus.begin( + object_manager = yield new DBusObjectManagerClient.for_bus.begin( BusType.SYSTEM, - GLib.DBusObjectManagerClientFlags.NONE, + DBusObjectManagerClientFlags.NONE, "org.bluez", "/", object_manager_proxy_get_type, @@ -54,7 +54,7 @@ public class Bluetooth.ObjectManager : Object { } } - //TODO: Do not rely on this when it is possible to do it natively in Vala + // TODO: Do not rely on this when it is possible to do it natively in Vala [CCode (cname="bluetooth_device_proxy_get_type")] extern static GLib.Type get_device_proxy_type(); @@ -62,7 +62,7 @@ public class Bluetooth.ObjectManager : Object { extern static GLib.Type get_adapter_proxy_type(); private GLib.Type object_manager_proxy_get_type(DBusObjectManagerClient manager, string object_path, string? interface_name) { - if (interface_name == null) return typeof (GLib.DBusObjectProxy); + if (interface_name == null) return typeof (DBusObjectProxy); switch (interface_name) { case "org.bluez.Device1": @@ -70,7 +70,7 @@ public class Bluetooth.ObjectManager : Object { case "org.bluez.Adapter1": return get_adapter_proxy_type(); default: - return typeof(GLib.DBusProxy); + return typeof(DBusProxy); } } @@ -84,14 +84,14 @@ public class Bluetooth.ObjectManager : Object { "RegisterAgent", // TODO: Do we need to worry about unregistering? new Variant("(o)", "/org/bluez/obex/budgie"), null, - GLib.DBusCallFlags.NONE, + DBusCallFlags.NONE, -1); } catch (Error e) { critical("Error registering Obex agent manager: %s", e.message); } } - private void on_interface_added(GLib.DBusObject object, GLib.DBusInterface iface) { + private void on_interface_added(DBusObject object, DBusInterface iface) { if (iface is Bluetooth.Device) { unowned var device = (Bluetooth.Device) iface; device_added(device); @@ -99,7 +99,7 @@ public class Bluetooth.ObjectManager : Object { unowned var adapter = (Bluetooth.Adapter) iface; has_object = true; ((DBusProxy) adapter).g_properties_changed.connect((changed, invalid) => { - var discovering = changed.lookup_value("Discovering", GLib.VariantType.BOOLEAN); + var discovering = changed.lookup_value("Discovering", VariantType.BOOLEAN); if (discovering != null) { status_discovering(); } @@ -107,7 +107,7 @@ public class Bluetooth.ObjectManager : Object { } } - private void on_interface_removed(GLib.DBusObject object, GLib.DBusInterface iface) { + private void on_interface_removed(DBusObject object, DBusInterface iface) { if (iface is Bluetooth.Device) { device_removed((Bluetooth.Device) iface); } else if (iface is Bluetooth.Adapter) { @@ -119,7 +119,7 @@ public class Bluetooth.ObjectManager : Object { var adapters = new Gee.LinkedList(); object_manager.get_objects().foreach((object) => { - GLib.DBusInterface? iface = object.get_interface("org.bluez.Adapter1"); + DBusInterface? iface = object.get_interface("org.bluez.Adapter1"); if (iface == null) return; adapters.add(((Bluetooth.Adapter) iface)); @@ -132,7 +132,7 @@ public class Bluetooth.ObjectManager : Object { var devices = new Gee.LinkedList(); object_manager.get_objects().foreach((object) => { - GLib.DBusInterface? iface = object.get_interface("org.bluez.Device1"); + DBusInterface? iface = object.get_interface("org.bluez.Device1"); if (iface == null) return; devices.add(((Bluetooth.Device) iface)); @@ -183,7 +183,7 @@ public class Bluetooth.ObjectManager : Object { } public Bluetooth.Adapter? get_adapter_from_path(string path) { - GLib.DBusObject? object = object_manager.get_object(path); + DBusObject? object = object_manager.get_object(path); if (object != null) { return (Bluetooth.Adapter?) object.get_interface("org.bluez.Adapter1"); diff --git a/src/dialogs/sendto/Services/ObexAgent.vala b/src/dialogs/sendto/Services/ObexAgent.vala index 58f42c245..3405ab820 100644 --- a/src/dialogs/sendto/Services/ObexAgent.vala +++ b/src/dialogs/sendto/Services/ObexAgent.vala @@ -18,7 +18,7 @@ public errordomain BluezObexError { [DBus (name = "org.bluez.obex.Agent1")] public class Bluetooth.Obex.Agent : GLib.Object { /* one confirmation for many files in one session */ - private GLib.ObjectPath many_files; + private ObjectPath many_files; public signal void response_notify(string address, ObjectPath object_path); public signal void response_accepted(string address, ObjectPath object_path); @@ -29,12 +29,12 @@ public class Bluetooth.Obex.Agent : GLib.Object { Bus.own_name( BusType.SESSION, "org.bluez.obex.Agent1", - GLib.BusNameOwnerFlags.NONE, + BusNameOwnerFlags.NONE, on_name_get ); } - private void on_name_get(GLib.DBusConnection conn) { + private void on_name_get(DBusConnection conn) { try { conn.register_object("/org/bluez/obex/budgie", this); } catch (Error e) { @@ -42,7 +42,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { } } - public void transfer_active(string session_path) throws GLib.Error { + public void transfer_active(string session_path) throws Error { transfer_view(session_path); } @@ -55,7 +55,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { * agent, because when this method gets called it has * already been unregistered. */ - public void release() throws GLib.Error {} + public void release() throws Error {} /** * authorize_push: @@ -70,7 +70,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { * property that contains the default location and name * that can be returned. */ - public async string authorize_push(GLib.ObjectPath object_path) throws Error { + public async string authorize_push(ObjectPath object_path) throws Error { SourceFunc callback = authorize_push.callback; BluezObexError? obex_error = null; Bluetooth.Obex.Transfer transfer = Bus.get_proxy_sync(BusType.SESSION, "org.bluez.obex", object_path); @@ -107,10 +107,12 @@ public class Bluetooth.Obex.Agent : GLib.Object { if (many_files == object_path) { Idle.add(()=>{ response_accepted(session.destination, object_path); + if (callback != null) { Idle.add((owned) callback); } - return GLib.Source.REMOVE; + + return Source.REMOVE; }); } else { // Not multple files, ask to accept or reject @@ -132,7 +134,7 @@ public class Bluetooth.Obex.Agent : GLib.Object { * request failed before a reply was returned. It cancels * the previous request. */ - public void cancel() throws GLib.Error { + public void cancel() throws Error { response_canceled(); } } From c3a682d53f8df362640a8e69b2e6578441285993 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Fri, 15 Sep 2023 14:14:38 -0400 Subject: [PATCH 73/81] sendto: Use the correct property when looking for changes Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/DeviceRow.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/sendto/Dialog/DeviceRow.vala b/src/dialogs/sendto/Dialog/DeviceRow.vala index 4fa6f6aac..a5a1b6d91 100644 --- a/src/dialogs/sendto/Dialog/DeviceRow.vala +++ b/src/dialogs/sendto/Dialog/DeviceRow.vala @@ -85,7 +85,7 @@ public class DeviceRow : Gtk.ListBoxRow { }); ((DBusProxy) device).g_properties_changed.connect((changed, invalid) => { - var name = changed.lookup_value("Name", new VariantType("s")); + var name = changed.lookup_value("Alias", new VariantType("s")); if (name != null) { label.label = device.alias; } From 05e95d213fb1c3595c9748f8ccd2c6c15fff2385 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Wed, 10 Jan 2024 17:22:31 -0500 Subject: [PATCH 74/81] sendto: Refactor format_time function Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/FileReceiver.vala | 26 ++++++++++----------- src/dialogs/sendto/Dialog/FileSender.vala | 26 ++++++++++----------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala index a8887e935..a876d617f 100644 --- a/src/dialogs/sendto/Dialog/FileReceiver.vala +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -248,23 +248,23 @@ public class FileReceiver : Gtk.Dialog { private string format_time(int seconds) { if (seconds < 0) seconds = 0; - if (seconds < 60) return ngettext("%d second", "%d seconds", seconds).printf(seconds); - int minutes; - if (seconds < 60 * 60) { - minutes = (seconds + 30) / 60; - return ngettext("%d minute", "%d minutes", minutes).printf(minutes); + var hours = seconds / 3600; + var minutes = (seconds - hours * 3600) / 60; + seconds = seconds - hours * 3600 - minutes * 60; + + if (hours > 0) { + var h = ngettext("%u hour", "%u hours", hours).printf(hours); + var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + return "%s, %s".printf(h, m); } - int hours = seconds / (60 * 60); - if (seconds < 60 * 60 * 4) { - minutes = (seconds - hours * 60 * 60 + 30) / 60; - string h = ngettext("%u hour", "%u hours", hours).printf(hours); - string m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); - ///TRANSLATORS: For example "1 hour, 8 minutes". - return _("%s, %s").printf(h, m); + if (minutes > 0) { + var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + var s = ngettext("%u second", "%u seconds", seconds).printf(seconds); + return "%s, %s".printf(m, s); } - return ngettext("about %d hour", "about %d hours", hours).printf(hours); + return ngettext("%d second", "%d seconds", seconds).printf(seconds); } } diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala index cbe4b6cca..f40de0349 100644 --- a/src/dialogs/sendto/Dialog/FileSender.vala +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -386,24 +386,24 @@ public class FileSender : Gtk.Dialog { private string format_time(int seconds) { if (seconds < 0) seconds = 0; - if (seconds < 60) return ngettext("%d second", "%d seconds", seconds).printf(seconds); - int minutes; - if (seconds < 60 * 60) { - minutes = (seconds + 30) / 60; - return ngettext("%d minute", "%d minutes", minutes).printf(minutes); + var hours = seconds / 3600; + var minutes = (seconds - hours * 3600) / 60; + seconds = seconds - hours * 3600 - minutes * 60; + + if (hours > 0) { + var h = ngettext("%u hour", "%u hours", hours).printf(hours); + var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + return "%s, %s".printf(h, m); } - int hours = seconds / (60 * 60); - if (seconds < 60 * 60 * 4) { - minutes = (seconds - hours * 60 * 60 + 30) / 60; - string h = ngettext("%u hour", "%u hours", hours).printf(hours); - string m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); - ///TRANSLATORS: For example "1 hour, 8 minutes". - return _("%s, %s").printf(h, m); + if (minutes > 0) { + var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + var s = ngettext("%u second", "%u seconds", seconds).printf(seconds); + return "%s, %s".printf(m, s); } - return ngettext("about %d hour", "about %d hours", hours).printf(hours); + return ngettext("%d second", "%d seconds", seconds).printf(seconds); } private bool try_next_file() { From 1a5509330c8a47307d49ea2c59b6c13ea662c8f8 Mon Sep 17 00:00:00 2001 From: Evan Maddock <5157277+EbonJaeger@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:26:15 -0500 Subject: [PATCH 75/81] Apply suggestions from code review Co-authored-by: Joshua Strobl --- src/dialogs/sendto/Dialog/ScanDialog.vala | 24 +++++-------------- src/dialogs/sendto/Services/Manager.vala | 1 - .../applets/status/BluetoothIndicator.vala | 13 +++++----- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/dialogs/sendto/Dialog/ScanDialog.vala b/src/dialogs/sendto/Dialog/ScanDialog.vala index e7eadbc71..16eb47fda 100644 --- a/src/dialogs/sendto/Dialog/ScanDialog.vala +++ b/src/dialogs/sendto/Dialog/ScanDialog.vala @@ -208,29 +208,17 @@ public class ScanDialog : Gtk.Dialog { unowned Bluetooth.Device device1 = row1.device; unowned Bluetooth.Device device2 = row2.device; - if (device1.paired && !device2.paired) { - return -1; - } + if (device1.paired && !device2.paired) return -1; - if (!device1.paired && device2.paired) { - return 1; - } + if (!device1.paired && device2.paired) return 1; - if (device1.connected && !device2.connected) { - return -1; - } + if (device1.connected && !device2.connected) return -1; - if (!device1.connected && device2.connected) { - return 1; - } + if (!device1.connected && device2.connected) return 1; - if (device1.name != null && device2.name == null) { - return -1; - } + if (device1.name != null && device2.name == null) return -1; - if (device1.name == null && device2.name != null) { - return 1; - } + if (device1.name == null && device2.name != null) return 1; var name1 = device1.name ?? device1.address; var name2 = device2.name ?? device2.address; diff --git a/src/dialogs/sendto/Services/Manager.vala b/src/dialogs/sendto/Services/Manager.vala index 0d9aa9e75..7105fd431 100644 --- a/src/dialogs/sendto/Services/Manager.vala +++ b/src/dialogs/sendto/Services/Manager.vala @@ -54,7 +54,6 @@ public class Bluetooth.ObjectManager : Object { } } - // TODO: Do not rely on this when it is possible to do it natively in Vala [CCode (cname="bluetooth_device_proxy_get_type")] extern static GLib.Type get_device_proxy_type(); diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index d6d7d7cf8..15d73f465 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -659,14 +659,13 @@ public class BTDeviceRow : ListBoxRow { } private void update_status() { - if (device.connected) { - status_label.set_text(_("Connected")); - connection_button.show(); - activatable = false; - } else { - status_label.set_text(_("Disconnected")); + activatable = !device.connected; + status_label.set_text(activatable ? _("Disconnected") : _("Connected")); + + if (activatable) { connection_button.hide(); - activatable = true; + } else { + connection_button.show(); } // Update the name if changed From bac7e640465c9b1b5b04598f26707e4b2b85cc4a Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Fri, 12 Jan 2024 14:23:28 -0500 Subject: [PATCH 76/81] sendto: Add new base dialog class Both the send and receive dialogs are almost the same, the main difference being in the background implementation and wording of strings. This causes some duplicated logic, especially around the formatting of times remaining. This adds a new base class that both dialogs can derive from while still having their own implementations for doing the work, leading to less duplicated code. Signed-off-by: Evan Maddock --- src/dialogs/sendto/Dialog/BaseDialog.vala | 182 ++++++++++++++++++++ src/dialogs/sendto/Dialog/FileReceiver.vala | 162 +---------------- src/dialogs/sendto/Dialog/FileSender.vala | 166 +----------------- src/dialogs/sendto/meson.build | 1 + 4 files changed, 192 insertions(+), 319 deletions(-) create mode 100644 src/dialogs/sendto/Dialog/BaseDialog.vala diff --git a/src/dialogs/sendto/Dialog/BaseDialog.vala b/src/dialogs/sendto/Dialog/BaseDialog.vala new file mode 100644 index 000000000..64ce30f77 --- /dev/null +++ b/src/dialogs/sendto/Dialog/BaseDialog.vala @@ -0,0 +1,182 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +public abstract class BaseDialog : Gtk.Dialog { + public Bluetooth.Obex.Transfer transfer; + public Bluetooth.Device device; + + protected int start_time = 0; + protected uint64 total_size = 0; + + protected Gtk.ProgressBar progress_bar; + protected Gtk.Label device_label; + protected Gtk.Label directory_label; + protected Gtk.Label progress_label; + protected Gtk.Label filename_label; + protected Gtk.Label rate_label; + protected Gtk.Image device_image; + + BaseDialog(Gtk.Application application) { + Object(application: application, resizable: false); + } + + construct { + var icon_image = new Gtk.Image.from_icon_name("bluetooth-active", Gtk.IconSize.DIALOG) { + valign = Gtk.Align.END, + halign = Gtk.Align.END, + }; + + device_image = new Gtk.Image() { + valign = Gtk.Align.END, + halign = Gtk.Align.END, + }; + + var overlay = new Gtk.Overlay() { + margin_right = 12, + }; + overlay.add(icon_image); + overlay.add_overlay(device_image); + + directory_label = new Gtk.Label(null) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + device_label = new Gtk.Label(null) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + filename_label = new Gtk.Label(Markup.printf_escaped("%s:", _("File name"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + rate_label = new Gtk.Label(Markup.printf_escaped("%s:", _("Transfer rate"))) { + max_width_chars = 45, + wrap = true, + xalign = 0, + use_markup = true, + }; + + progress_bar = new Gtk.ProgressBar() { + hexpand = true, + margin_top = 4, + margin_bottom = 4, + }; + + progress_label = new Gtk.Label(null) { + max_width_chars = 45, + hexpand = false, + wrap = true, + xalign = 0, + margin_bottom = 4, + }; + + var message_grid = new Gtk.Grid() { + column_spacing = 0, + row_spacing = 4, + width_request = 450, + margin = 10, + }; + + message_grid.attach(overlay, 0, 0, 1, 3); + message_grid.attach(directory_label, 1, 0, 1, 1); + message_grid.attach(device_label, 1, 1, 1, 1); + message_grid.attach(filename_label, 1, 2, 1, 1); + message_grid.attach(rate_label, 1, 3, 1, 1); + message_grid.attach(progress_bar, 1, 4, 1, 1); + message_grid.attach(progress_label, 1, 5, 1, 1); + + get_content_area().add(message_grid); + + // Now add the dialog buttons + add_button(_("Close"), Gtk.ResponseType.CLOSE); + var reject_transfer = add_button(_("Cancel"), Gtk.ResponseType.CANCEL); + reject_transfer.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + // Hook up the responses + response.connect((response_id) => { + if (response_id == Gtk.ResponseType.CANCEL) { + // Cancel the current transfer if it is active + if (transfer != null && transfer.status == "active") { + try { + transfer.cancel(); + } catch (Error e) { + warning("Error cancelling Bluetooth transfer: %s", e.message); + } + } + + destroy(); + } else { + // Close button clicked, hide or close + if (transfer.status == "active") { + hide_on_delete(); + } else { + destroy(); + } + } + }); + + delete_event.connect(() => { + if (transfer.status == "active") { + return hide_on_delete(); + } else { + destroy(); + } + }); + } + + protected void on_transfer_progress(uint64 transferred) { + progress_bar.fraction = (double) transferred / (double) total_size; + int current_time = (int) get_real_time(); + int elapsed_time = (current_time - start_time) / 1000000; + + if (current_time < start_time + 1000000) return; + if (elapsed_time == 0) return; + + uint64 transfer_rate = transferred / elapsed_time; + + if (transfer_rate == 0) return; + + rate_label.label = Markup.printf_escaped(_("Transfer rate: %s / s"), format_size(transfer_rate)); + uint64 remaining_time = (total_size - transferred) / transfer_rate; + progress_label.label = _("%s / %s: Time remaining: %s").printf(format_size(transferred), format_size(total_size), format_time((int) remaining_time)); + } + + protected string format_time(int seconds) { + if (seconds < 0) seconds = 0; + + var hours = seconds / 3600; + var minutes = (seconds - hours * 3600) / 60; + seconds = seconds - hours * 3600 - minutes * 60; + + if (hours > 0) { + var h = ngettext("%u hour", "%u hours", hours).printf(hours); + var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + return "%s, %s".printf(h, m); + } + + if (minutes > 0) { + var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); + var s = ngettext("%u second", "%u seconds", seconds).printf(seconds); + return "%s, %s".printf(m, s); + } + + return ngettext("%d second", "%d seconds", seconds).printf(seconds); + } +} diff --git a/src/dialogs/sendto/Dialog/FileReceiver.vala b/src/dialogs/sendto/Dialog/FileReceiver.vala index a876d617f..552a3b19b 100644 --- a/src/dialogs/sendto/Dialog/FileReceiver.vala +++ b/src/dialogs/sendto/Dialog/FileReceiver.vala @@ -9,25 +9,12 @@ * (at your option) any later version. */ -public class FileReceiver : Gtk.Dialog { - public Bluetooth.Device device { get; set; } +public class FileReceiver : BaseDialog { public string session_path { get; set; } - public Bluetooth.Obex.Transfer transfer; - - private Gtk.ProgressBar progress_bar; - private Gtk.Label device_label; - private Gtk.Label directory_label; - private Gtk.Label progress_label; - private Gtk.Label filename_label; - private Gtk.Label rate_label; - private Gtk.Image device_image; - private Notification notification; private string file_name = ""; - private int start_time = 0; - private uint64 total_size = 0; public FileReceiver(Gtk.Application application) { Object(application: application, resizable: false); @@ -35,114 +22,6 @@ public class FileReceiver : Gtk.Dialog { construct { title = _("Bluetooth File Transfer"); - - notification = new Notification("Bluetooth"); - notification.set_priority(NotificationPriority.NORMAL); - - var icon_image = new Gtk.Image.from_icon_name("bluetooth-active", Gtk.IconSize.DIALOG) { - valign = Gtk.Align.END, - halign = Gtk.Align.END, - }; - - device_image = new Gtk.Image() { - valign = Gtk.Align.END, - halign = Gtk.Align.END, - }; - - var overlay = new Gtk.Overlay() { - margin_right = 12, - }; - overlay.add(icon_image); - overlay.add_overlay(device_image); - - device_label = new Gtk.Label(null) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - directory_label = new Gtk.Label(null) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - filename_label = new Gtk.Label(Markup.printf_escaped("%s:", _("File name"))) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - rate_label = new Gtk.Label(Markup.printf_escaped("%s:", _("Transfer rate"))) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - progress_bar = new Gtk.ProgressBar() { - hexpand = true, - margin_top = 4, - margin_bottom = 4, - }; - - progress_label = new Gtk.Label(null) { - max_width_chars = 45, - hexpand = false, - wrap = true, - xalign = 0, - margin_bottom = 4, - }; - - var message_grid = new Gtk.Grid() { - column_spacing = 0, - row_spacing = 4, - width_request = 450, - margin = 10, - }; - - message_grid.attach(overlay, 0, 0, 1, 3); - message_grid.attach(device_label, 1, 0, 1, 1); - message_grid.attach(directory_label, 1, 1, 1, 1); - message_grid.attach(filename_label, 1, 2, 1, 1); - message_grid.attach(rate_label, 1, 3, 1, 1); - message_grid.attach(progress_bar, 1, 4, 1, 1); - message_grid.attach(progress_label, 1, 5, 1, 1); - - get_content_area().add(message_grid); - - // Now add the dialog buttons - add_button(_("Close"), Gtk.ResponseType.CLOSE); - var reject_button = add_button(_("Reject"), Gtk.ResponseType.REJECT); - reject_button.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - - // Hook up the responses - response.connect((response_id) => { - if (response_id == Gtk.ResponseType.REJECT) { - // Cancel the current transfer if it is active - try { - transfer.cancel(); - } catch (Error e) { - warning("Error rejecting Bluetooth transfer: %s", e.message); - } - - destroy(); - } else { - // Close button clicked, hide - hide_on_delete(); - } - }); - - delete_event.connect(() => { - if (transfer.status == "active") { - return hide_on_delete(); - } else { - return false; - } - }); } public void set_transfer(Bluetooth.Device device, string path) { @@ -228,43 +107,4 @@ public class FileReceiver : Gtk.Dialog { return File.new_for_path(name + " " + time + ext); } - - private void on_transfer_progress(uint64 transferred) { - progress_bar.fraction = (double) transferred / (double) total_size; - int current_time = (int) get_real_time(); - int elapsed_time = (current_time - start_time) / 1000000; - - if (current_time < start_time + 1000000) return; - if (elapsed_time == 0) return; - - uint64 transfer_rate = transferred / elapsed_time; - - if (transfer_rate == 0) return; - - rate_label.label = Markup.printf_escaped(_("Transfer rate: %s / s"), format_size(transfer_rate)); - uint64 remaining_time = (total_size - transferred) / transfer_rate; - progress_label.label = _("%s of %s received. Time remaining: %s").printf(format_size(transferred), format_size(total_size), format_time((int) remaining_time)); - } - - private string format_time(int seconds) { - if (seconds < 0) seconds = 0; - - var hours = seconds / 3600; - var minutes = (seconds - hours * 3600) / 60; - seconds = seconds - hours * 3600 - minutes * 60; - - if (hours > 0) { - var h = ngettext("%u hour", "%u hours", hours).printf(hours); - var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); - return "%s, %s".printf(h, m); - } - - if (minutes > 0) { - var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); - var s = ngettext("%u second", "%u seconds", seconds).printf(seconds); - return "%s, %s".printf(m, s); - } - - return ngettext("%d second", "%d seconds", seconds).printf(seconds); - } } diff --git a/src/dialogs/sendto/Dialog/FileSender.vala b/src/dialogs/sendto/Dialog/FileSender.vala index f40de0349..0f1cc19e2 100644 --- a/src/dialogs/sendto/Dialog/FileSender.vala +++ b/src/dialogs/sendto/Dialog/FileSender.vala @@ -9,14 +9,9 @@ * (at your option) any later version. */ -public class FileSender : Gtk.Dialog { - public Bluetooth.Obex.Transfer transfer; - public Bluetooth.Device device; - +public class FileSender : BaseDialog { private int current_file = 0; private int total_files = 0; - private uint64 total_size = 0; - private int start_time = 0; private DBusConnection connection; private DBusProxy client_proxy; @@ -26,14 +21,6 @@ public class FileSender : Gtk.Dialog { private Gtk.ListStore file_store; - private Gtk.Label path_label; - private Gtk.Label device_label; - private Gtk.Label filename_label; - private Gtk.Label rate_label; - private Gtk.Label progress_label; - private Gtk.ProgressBar progress_bar; - private Gtk.Image icon_label; - public FileSender(Gtk.Application application) { Object(application: application, resizable: false); } @@ -43,116 +30,16 @@ public class FileSender : Gtk.Dialog { file_store = new Gtk.ListStore(1, typeof(GLib.File)); - var icon_image = new Gtk.Image.from_icon_name("bluetooth-active", Gtk.IconSize.DIALOG) { - valign = Gtk.Align.END, - halign = Gtk.Align.END, - }; - - icon_label = new Gtk.Image() { - valign = Gtk.Align.END, - halign = Gtk.Align.END, - }; - - var overlay = new Gtk.Overlay() { - margin_right = 12, - }; - overlay.add(icon_image); - overlay.add_overlay(icon_label); - - path_label = new Gtk.Label(Markup.printf_escaped("%s:", _("From"))) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - device_label = new Gtk.Label(Markup.printf_escaped("%s:", _("To"))) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - filename_label = new Gtk.Label(Markup.printf_escaped("%s:", _("File name"))) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - rate_label = new Gtk.Label(Markup.printf_escaped("%s:", _("Transfer rate"))) { - max_width_chars = 45, - wrap = true, - xalign = 0, - use_markup = true, - }; - - progress_bar = new Gtk.ProgressBar() { - hexpand = true, - margin_top = 4, - margin_bottom = 4, - }; - - progress_label = new Gtk.Label(null) { - max_width_chars = 45, - hexpand = false, - wrap = true, - xalign = 0, - margin_bottom = 4, - }; - - var message_grid = new Gtk.Grid() { - column_spacing = 0, - row_spacing = 4, - width_request = 450, - margin = 10, - }; - - message_grid.attach(overlay, 0, 0, 1, 3); - message_grid.attach(path_label, 1, 0, 1, 1); - message_grid.attach(device_label, 1, 1, 1, 1); - message_grid.attach(filename_label, 1, 2, 1, 1); - message_grid.attach(rate_label, 1, 3, 1, 1); - message_grid.attach(progress_bar, 1, 4, 1, 1); - message_grid.attach(progress_label, 1, 5, 1, 1); - - get_content_area().add(message_grid); - - // Now add the dialog buttons - add_button(_("Close"), Gtk.ResponseType.CLOSE); - var reject_transfer = add_button(_("Cancel"), Gtk.ResponseType.CANCEL); - reject_transfer.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + directory_label.set_markup(Markup.printf_escaped("%s:", _("From"))); + device_label.set_markup(Markup.printf_escaped("%s:", _("To"))); // Hook up the responses response.connect((response_id) => { if (response_id == Gtk.ResponseType.CANCEL) { - // Cancel the current transfer if it is active + // Cancel the current session if it is active if (transfer != null && transfer.status == "active") { - try { - transfer.cancel(); - } catch (Error e) { - warning("Error cancelling Bluetooth transfer: %s", e.message); - } - remove_session.begin(); } - - destroy(); - } else { - // Close button clicked, hide or close - if (transfer.status == "active") { - hide_on_delete(); - } else { - destroy(); - } - } - }); - - delete_event.connect(() => { - if (transfer.status == "active") { - return hide_on_delete(); - } else { - destroy(); } }); } @@ -210,9 +97,9 @@ public class FileSender : Gtk.Dialog { ); // Update the labels - path_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), file_path.get_parent().get_path())); + directory_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), file_path.get_parent().get_path())); device_label.set_markup(GLib.Markup.printf_escaped(_("To: %s"), device.alias)); - icon_label.set_from_gicon(new ThemedIcon(device.icon == null ? "bluetooth-active" : device.icon), Gtk.IconSize.LARGE_TOOLBAR); + device_image.set_from_gicon(new ThemedIcon(device.icon == null ? "bluetooth-active" : device.icon), Gtk.IconSize.LARGE_TOOLBAR); progress_label.label = _("Trying to connect to %s…").printf(device.alias); // Prepare to send the file @@ -284,9 +171,9 @@ public class FileSender : Gtk.Dialog { private async void send_file() { // Update the labels - path_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), file_path.get_parent().get_path())); + directory_label.set_markup(GLib.Markup.printf_escaped(_("From: %s"), file_path.get_parent().get_path())); device_label.set_markup(GLib.Markup.printf_escaped(_("To: %s"), device.alias)); - icon_label.set_from_gicon(new ThemedIcon(device.icon ?? "bluetooth-active"), Gtk.IconSize.LARGE_TOOLBAR); + device_image.set_from_gicon(new ThemedIcon(device.icon ?? "bluetooth-active"), Gtk.IconSize.LARGE_TOOLBAR); progress_label.label = _("Waiting for acceptance on %s…").printf(device.alias); try { @@ -369,43 +256,6 @@ public class FileSender : Gtk.Dialog { } } - private void on_transfer_progress(uint64 transferred) { - progress_bar.fraction = (double) transferred / (double) total_size; - int current_time = (int) get_real_time(); - int elapsed_time = (current_time - start_time) / 1000000; - if (current_time < start_time + 1000000) return; - if (elapsed_time == 0) return; - - uint64 transfer_rate = transferred / elapsed_time; - if (transfer_rate == 0) return; - - rate_label.label = Markup.printf_escaped(_("Transfer rate: %s / s"), format_size(transfer_rate)); - uint64 remaining_time = (total_size - transferred) / transfer_rate; - progress_label.label = _("(%i/%i) %s of %s sent. Time remaining: %s").printf(current_file, total_files, format_size(transferred), format_size(total_size), format_time((int) remaining_time)); - } - - private string format_time(int seconds) { - if (seconds < 0) seconds = 0; - - var hours = seconds / 3600; - var minutes = (seconds - hours * 3600) / 60; - seconds = seconds - hours * 3600 - minutes * 60; - - if (hours > 0) { - var h = ngettext("%u hour", "%u hours", hours).printf(hours); - var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); - return "%s, %s".printf(h, m); - } - - if (minutes > 0) { - var m = ngettext("%u minute", "%u minutes", minutes).printf(minutes); - var s = ngettext("%u second", "%u seconds", seconds).printf(seconds); - return "%s, %s".printf(m, s); - } - - return ngettext("%d second", "%d seconds", seconds).printf(seconds); - } - private bool try_next_file() { Gtk.TreeIter iter; if (file_store.get_iter_from_string(out iter, current_file.to_string())) { diff --git a/src/dialogs/sendto/meson.build b/src/dialogs/sendto/meson.build index 687275676..e9e923575 100644 --- a/src/dialogs/sendto/meson.build +++ b/src/dialogs/sendto/meson.build @@ -1,5 +1,6 @@ sendto_sources = [ 'Application.vala', + 'Dialog/BaseDialog.vala', 'Dialog/DeviceRow.vala', 'Dialog/FileReceiver.vala', 'Dialog/FileSender.vala', From 81a2bfc19118fffbe9c6e54a48ade56ec337be2b Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 22 Jan 2024 11:29:45 -0500 Subject: [PATCH 77/81] bluetooth-indicator: Address feedback from fossfreedom Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 15d73f465..f3dd6fd66 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -174,15 +174,8 @@ public class BluetoothIndicator : Bin { wrap = true, }; - var placeholder_button = new Button.with_label(_("Open Bluetooth Settings")) { - relief = HALF, - }; - placeholder_button.get_style_context().add_class(STYLE_CLASS_SUGGESTED_ACTION); - placeholder_button.clicked.connect(on_settings_activate); - placeholder.pack_start(placeholder_label, false); placeholder.pack_start(placeholder_sublabel, false); - placeholder.pack_start(placeholder_button, false); placeholder.show_all(); // Without this, it never shows. Because... reasons? devices_box.set_placeholder(placeholder); scrolled_window.add(devices_box); @@ -192,7 +185,7 @@ public class BluetoothIndicator : Bin { add(ebox); box.pack_start(header); - box.pack_start(new Separator(HORIZONTAL), true, true, 2); + box.pack_start(new Separator(HORIZONTAL), true, true, 4); box.pack_start(scrolled_window); box.show_all(); popover.add(box); @@ -249,7 +242,21 @@ public class BluetoothIndicator : Bin { // Turn Bluetooth on or off var active = bluetooth_switch.active; client.set_airplane_mode(!active); // If the switch is active, then Bluetooth is enabled. So invert the value - send_button.sensitive = active; + send_button.sensitive = active && has_connected_devices(); + } + + private bool has_connected_devices() { + bool connected = false; + + foreach (var row in devices_box.get_children()) { + var child = row as BTDeviceRow; + if (child.device.connected) { + connected = true; + break; + } + } + + return connected; } private void add_device(Device1 device) { @@ -260,11 +267,13 @@ public class BluetoothIndicator : Bin { widget.properties_updated.connect(() => { devices_box.invalidate_filter(); devices_box.invalidate_sort(); + send_button.sensitive = has_connected_devices(); }); devices_box.add(widget); devices_box.invalidate_filter(); devices_box.invalidate_sort(); + send_button.sensitive = has_connected_devices(); } private void remove_device(Device1 device) { @@ -279,6 +288,7 @@ public class BluetoothIndicator : Bin { devices_box.invalidate_filter(); devices_box.invalidate_sort(); + send_button.sensitive = has_connected_devices(); } /** From 32cd3abac6a72ed9ca0f9d986ffaa77869f62084 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Mon, 22 Jan 2024 12:17:00 -0500 Subject: [PATCH 78/81] sendto, bluetooth-indicator: Support sending files to a specific device Signed-off-by: Evan Maddock --- src/dialogs/sendto/Application.vala | 63 ++++++++------ .../applets/status/BluetoothIndicator.vala | 84 ++++++++----------- 2 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/dialogs/sendto/Application.vala b/src/dialogs/sendto/Application.vala index 3b3cfc842..18470547b 100644 --- a/src/dialogs/sendto/Application.vala +++ b/src/dialogs/sendto/Application.vala @@ -13,6 +13,7 @@ public class SendtoApplication : Gtk.Application { private const OptionEntry[] OPTIONS = { { "daemon", 'd', 0, OptionArg.NONE, out silent, "Run the application in the background", null }, { "send", 'f', 0, OptionArg.NONE, out send, "Send a file via Bluetooth", null }, + { "device", 'a', 0, OptionArg.STRING, out device_addr, "Bluetooth device to send files to", null }, { "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Files to send via Bluetooth", null }, { null }, }; @@ -20,6 +21,7 @@ public class SendtoApplication : Gtk.Application { private static bool silent = true; private static bool send = false; private static bool active_once; + private static string? device_addr = null; [CCode (array_length = false, array_null_terminated = true)] private static string[]? arg_files = {}; @@ -95,37 +97,46 @@ public class SendtoApplication : Gtk.Application { // Still no files, exit if (files.length == 0) return 0; - // Create the Bluetooth scanner dialog if it doesn't yet exist - if (scan_dialog == null) { - scan_dialog = new ScanDialog(this, manager); + // Start and show the device scanner if we weren't given a + // Bluetooth device address + if (device_addr == null || device_addr == "") { + // Make sure we have a dialog object + if (scan_dialog == null) { + scan_dialog = new ScanDialog(this, manager); + + // Wait for asyncronous initialization before showing the dialog + Idle.add(() => { + scan_dialog.show_all(); + return Source.REMOVE; + }); + } else { + // Dialog already exists, present it + scan_dialog.present(); + } - // Wait for asyncronous initialization before showing the dialog - Idle.add(() => { - scan_dialog.show_all(); - return Source.REMOVE; + // Clear our pointer when the scan dialog is destroyed + scan_dialog.destroy.connect(() => { + scan_dialog = null; + }); + + // Send the files when a device has been selected + scan_dialog.send_file.connect((device) => { + device_addr = device.address; }); - } else { - // Dialog already exists, present it - scan_dialog.present(); } - // Clear our pointer when the scan dialog is destroyed - scan_dialog.destroy.connect(() => { - scan_dialog = null; - }); + var device = manager.get_device(device_addr); - // Send the files when a device has been selected - scan_dialog.send_file.connect((device) => { - if (!insert_sender(files, device)) { - file_sender = new FileSender(this); - file_sender.add_files(files, device); - file_senders.append(file_sender); - file_sender.show_all(); - file_sender.destroy.connect(() => { - file_senders.remove_link(file_senders.find(file_sender)); - }); - } - }); + // Send the file to the device + if (!insert_sender(files, device)) { + file_sender = new FileSender(this); + file_sender.add_files(files, device); + file_senders.append(file_sender); + file_sender.show_all(); + file_sender.destroy.connect(() => { + file_senders.remove_link(file_senders.find(file_sender)); + }); + } // Cleanup arg_files = {}; diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index f3dd6fd66..e5cabac53 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -21,7 +21,6 @@ public class BluetoothIndicator : Bin { public Budgie.Popover? popover = null; private ListBox? devices_box = null; - private Button? send_button = null; private Switch? bluetooth_switch = null; private Label? placeholder_label = null; private Label? placeholder_sublabel = null; @@ -109,13 +108,6 @@ public class BluetoothIndicator : Bin { }; switch_label.get_style_context().add_class(STYLE_CLASS_DIM_LABEL); - // Send button - send_button = new Button.from_icon_name("folder-download-symbolic", MENU) { - relief = NONE, - tooltip_text = _("Send files via Bluetooth"), - }; - send_button.clicked.connect(on_send_clicked); - // Settings button var settings_button = new Button.from_icon_name("preferences-system-symbolic", MENU) { tooltip_text = _("Bluetooth Settings") @@ -133,7 +125,6 @@ public class BluetoothIndicator : Bin { header.pack_start(switch_label); header.pack_end(bluetooth_switch, false, false); header.pack_end(settings_button, false, false); - header.pack_end(send_button, false, false); // Devices var scrolled_window = new ScrolledWindow(null, null) { @@ -204,27 +195,6 @@ public class BluetoothIndicator : Bin { return Gdk.EVENT_STOP; } - private void on_send_clicked() { - this.popover.hide(); - - string[] args = { "org.buddiesofbudgie.sendto", "-f" }; - var env = Environ.get(); - Pid pid; - - try { - Process.spawn_async( - null, - args, - env, - SEARCH_PATH_FROM_ENVP, - null, - out pid - ); - } catch (SpawnError e) { - warning("Error starting sendto: %s", e.message); - } - } - private void on_settings_activate() { this.popover.hide(); @@ -242,21 +212,6 @@ public class BluetoothIndicator : Bin { // Turn Bluetooth on or off var active = bluetooth_switch.active; client.set_airplane_mode(!active); // If the switch is active, then Bluetooth is enabled. So invert the value - send_button.sensitive = active && has_connected_devices(); - } - - private bool has_connected_devices() { - bool connected = false; - - foreach (var row in devices_box.get_children()) { - var child = row as BTDeviceRow; - if (child.device.connected) { - connected = true; - break; - } - } - - return connected; } private void add_device(Device1 device) { @@ -267,13 +222,11 @@ public class BluetoothIndicator : Bin { widget.properties_updated.connect(() => { devices_box.invalidate_filter(); devices_box.invalidate_sort(); - send_button.sensitive = has_connected_devices(); }); devices_box.add(widget); devices_box.invalidate_filter(); devices_box.invalidate_sort(); - send_button.sensitive = has_connected_devices(); } private void remove_device(Device1 device) { @@ -288,7 +241,6 @@ public class BluetoothIndicator : Bin { devices_box.invalidate_filter(); devices_box.invalidate_sort(); - send_button.sensitive = has_connected_devices(); } /** @@ -358,6 +310,7 @@ public class BTDeviceRow : ListBoxRow { private Revealer? revealer = null; private Spinner? spinner = null; private Label? status_label = null; + private Button? send_button = null; private Button? connection_button = null; private Revealer? progress_revealer = null; private Label? file_label = null; @@ -477,6 +430,31 @@ public class BTDeviceRow : ListBoxRow { var button_box = new Box(Orientation.HORIZONTAL, 0); + // Send button + send_button = new Button.from_icon_name("folder-download-symbolic") { + relief = ReliefStyle.HALF, + tooltip_text = _("Send file"), + }; + send_button.get_style_context().add_class("circular"); + send_button.clicked.connect(() => { + string[] args = { "org.buddiesofbudgie.sendto", "-a", device.address, "-f" }; + var env = Environ.get(); + Pid pid; + + try { + Process.spawn_async( + null, + args, + env, + SEARCH_PATH_FROM_ENVP, + null, + out pid + ); + } catch (SpawnError e) { + warning("Error starting sendto: %s", e.message); + } + }); + // Disconnect button connection_button = new Button.from_icon_name("bluetooth-disabled-symbolic", IconSize.BUTTON) { relief = ReliefStyle.HALF, @@ -488,6 +466,7 @@ public class BTDeviceRow : ListBoxRow { toggle_connection.begin(); }); + button_box.pack_start(send_button, false); button_box.pack_start(connection_button, false); // Progress stuff @@ -674,8 +653,17 @@ public class BTDeviceRow : ListBoxRow { if (activatable) { connection_button.hide(); + send_button.hide(); } else { connection_button.show(); + + // We only want to show the send button if the device + // can actually receive files. + if ((device.@class & 0x20C) == 0 || // Smartphone + (device.@class & 0x104) == 0 || // Desktop workstation + (device.@class & 0x10C) == 0) { // Laptop + send_button.show(); + } } // Update the name if changed From 4a16257ec551f44838d38cdf705305ea1b2a82c7 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 23 Jan 2024 12:35:48 -0500 Subject: [PATCH 79/81] bluetooth-indicator: Fix button relief and device class checks Signed-off-by: Evan Maddock --- .../applets/status/BluetoothIndicator.vala | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index e5cabac53..f81bf3435 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -110,10 +110,9 @@ public class BluetoothIndicator : Bin { // Settings button var settings_button = new Button.from_icon_name("preferences-system-symbolic", MENU) { + relief = ReliefStyle.NONE, tooltip_text = _("Bluetooth Settings") }; - settings_button.get_style_context().add_class(STYLE_CLASS_FLAT); - settings_button.get_style_context().remove_class(STYLE_CLASS_BUTTON); settings_button.clicked.connect(on_settings_activate); // Bluetooth switch @@ -301,6 +300,9 @@ public class BluetoothIndicator : Bin { public class BTDeviceRow : ListBoxRow { private const string OBEX_AGENT = "org.bluez.obex.Agent1"; private const string OBEX_PATH = "/org/bluez/obex/budgie"; + private const uint32 SMARTPHONE_MASK = 0x20C; + private const uint32 DESKTOP_MASK = 0x104; + private const uint32 LAPTOP_MASK = 0x10C; private Image? image = null; private Label? name_label = null; @@ -428,11 +430,13 @@ public class BTDeviceRow : ListBoxRow { status_box.pack_start(status_label, false); status_box.pack_start(revealer, false); - var button_box = new Box(Orientation.HORIZONTAL, 0); + var button_box = new Box(Orientation.HORIZONTAL, 0) { + homogeneous = false, + }; // Send button send_button = new Button.from_icon_name("folder-download-symbolic") { - relief = ReliefStyle.HALF, + relief = ReliefStyle.NONE, tooltip_text = _("Send file"), }; send_button.get_style_context().add_class("circular"); @@ -457,7 +461,7 @@ public class BTDeviceRow : ListBoxRow { // Disconnect button connection_button = new Button.from_icon_name("bluetooth-disabled-symbolic", IconSize.BUTTON) { - relief = ReliefStyle.HALF, + relief = ReliefStyle.NONE, tooltip_text = _("Disconnect"), }; connection_button.get_style_context().add_class("circular"); @@ -466,8 +470,8 @@ public class BTDeviceRow : ListBoxRow { toggle_connection.begin(); }); - button_box.pack_start(send_button, false); - button_box.pack_start(connection_button, false); + button_box.pack_start(send_button, true, true, 0); + button_box.pack_start(connection_button, true, true, 0); // Progress stuff progress_revealer = new Revealer() { @@ -523,6 +527,7 @@ public class BTDeviceRow : ListBoxRow { add(box); show_all(); + send_button.hide(); update_status(); } @@ -648,22 +653,22 @@ public class BTDeviceRow : ListBoxRow { } private void update_status() { - activatable = !device.connected; - status_label.set_text(activatable ? _("Disconnected") : _("Connected")); + status_label.set_text(device.connected ? _("Connected") : _("Disconnected")); - if (activatable) { - connection_button.hide(); - send_button.hide(); - } else { + if (device.connected) { connection_button.show(); // We only want to show the send button if the device // can actually receive files. - if ((device.@class & 0x20C) == 0 || // Smartphone - (device.@class & 0x104) == 0 || // Desktop workstation - (device.@class & 0x10C) == 0) { // Laptop + message("%x", device.@class & 0x20C); + if ((device.@class & SMARTPHONE_MASK) == SMARTPHONE_MASK || + (device.@class & DESKTOP_MASK) == DESKTOP_MASK || + (device.@class & LAPTOP_MASK) == LAPTOP_MASK) { send_button.show(); } + } else { + connection_button.hide(); + send_button.hide(); } // Update the name if changed From 1a463062a08f15c5e819c2d4371811da3fc28724 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 23 Jan 2024 13:29:24 -0500 Subject: [PATCH 80/81] bluetooth-indicator: Hide the device's battery status when disconnected Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index f81bf3435..3049fc898 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -666,9 +666,12 @@ public class BTDeviceRow : ListBoxRow { (device.@class & LAPTOP_MASK) == LAPTOP_MASK) { send_button.show(); } + + update_battery(); } else { connection_button.hide(); send_button.hide(); + battery_revealer.reveal_child = false; } // Update the name if changed From 891839ef3a50cb036362e7d4280db084767bbbe9 Mon Sep 17 00:00:00 2001 From: Evan Maddock Date: Tue, 23 Jan 2024 13:31:07 -0500 Subject: [PATCH 81/81] bluetooth-indicator: Remove debugging message Signed-off-by: Evan Maddock --- src/panel/applets/status/BluetoothIndicator.vala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/panel/applets/status/BluetoothIndicator.vala b/src/panel/applets/status/BluetoothIndicator.vala index 3049fc898..56ec4829d 100644 --- a/src/panel/applets/status/BluetoothIndicator.vala +++ b/src/panel/applets/status/BluetoothIndicator.vala @@ -660,7 +660,6 @@ public class BTDeviceRow : ListBoxRow { // We only want to show the send button if the device // can actually receive files. - message("%x", device.@class & 0x20C); if ((device.@class & SMARTPHONE_MASK) == SMARTPHONE_MASK || (device.@class & DESKTOP_MASK) == DESKTOP_MASK || (device.@class & LAPTOP_MASK) == LAPTOP_MASK) {