diff --git a/Cargo.toml b/Cargo.toml index a365b60..b54f4d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ debug = true [profile.release] opt-level = "s" -lto = "thin" +lto = "off" # [patch.'https://github.com/smithay/client-toolkit/'] # smithay-client-toolkit = { git = "https://github.com/smithay/client-toolkit//", rev = "c583de8" } diff --git a/cosmic-settings/src/pages/bluetooth/mod.rs b/cosmic-settings/src/pages/bluetooth/mod.rs index aca040c..2abc46a 100644 --- a/cosmic-settings/src/pages/bluetooth/mod.rs +++ b/cosmic-settings/src/pages/bluetooth/mod.rs @@ -38,20 +38,128 @@ enum Dialog { } #[derive(Default)] -pub struct Page { +pub struct Model { active: Active, - connection: Option, - dialog: Option, adapters: HashMap, selected_adapter: Option, - heading: String, devices: HashMap, + popup_device: Option, + popup_setting: bool, +} + +impl Model { + pub fn clear(&mut self) { + self.adapters.clear(); + self.selected_adapter = None; + self.devices.clear(); + self.popup_device = None; + self.popup_setting = false; + } + + /// Updates known adapters, and selects an adapter if no adapter was selected. + pub fn set_adapters( + &mut self, + adapters: HashMap, + ) -> Option { + self.adapters = adapters; + self.update_status(); + + if self.selected_adapter.is_none() && self.adapters.len() == 1 { + return self.adapters.keys().next().cloned(); + } + + None + } + + fn update_status(&mut self) { + self.active = if let Some((_, adapter)) = self.get_selected_adapter() { + adapter.enabled + } else if self + .adapters + .values() + .any(|adapter| matches!(adapter.enabled, Active::Enabled | Active::Enabling)) + { + Active::Enabled + } else { + Active::Disabled + } + } + + // Updates an adapter, and initiates discovery if an existing adapter is enabled. + fn updated_adapter( + &mut self, + path: OwnedObjectPath, + update: Vec, + connection: zbus::Connection, + ) -> Option + Send + 'static> { + if let Some(existing) = self.adapters.get_mut(&path) { + tracing::debug!("Adapter {} updated: {update:#?}", existing.address); + existing.update(update); + } + + self.update_status(); + + if let Some((path, existing)) = self.get_selected_adapter_mut() { + if existing.enabled == Active::Enabled && existing.scanning == Active::Disabled { + existing.scanning = Active::Enabling; + return Some(start_discovery(connection, path)); + } + } + + None + } + + fn adapter_connected(&self, adapter_path: &OwnedObjectPath) -> bool { + self.devices + .iter() + .any(|(path, device)| path.starts_with(adapter_path.as_str()) && device.is_connected()) + } + + fn get_selected_adapter(&self) -> Option<(&'_ OwnedObjectPath, &'_ Adapter)> { + if let Some(iface) = &self.selected_adapter { + self.adapters.get_key_value(iface) + } else { + None + } + } + + fn devices_for_adapter<'a>( + &'a self, + adapter_path: &'a OwnedObjectPath, + ) -> impl Iterator { + self.devices.iter().filter_map(|(path, device)| { + if device.adapter.eq(adapter_path) { + Some((path, device)) + } else { + None + } + }) + } + + fn get_selected_adapter_mut(&mut self) -> Option<(OwnedObjectPath, &'_ mut Adapter)> { + if let Some(path) = &self.selected_adapter { + self.adapters + .get_mut(path) + .map(|adapter| (path.to_owned(), adapter)) + } else { + None + } + } +} + +#[derive(Default)] +pub struct Page { + model: Model, + + connection: Option, + dialog: Option, + heading: String, + // Set to true when the org.bluez dbus service is unknown. bluez_service_unknown: bool, service_is_enabled: bool, service_is_active: bool, - popup_setting: bool, - popup_device: Option, + subscription: Option>, } @@ -103,11 +211,7 @@ impl page::Page for Page { }); } - self.adapters.clear(); - self.selected_adapter = None; - self.devices.clear(); - self.popup_device = None; - self.popup_setting = false; + self.model.clear(); Task::none() } @@ -230,12 +334,12 @@ impl Page { Event::Ok => {} Event::SetDevices(devices) => { - self.devices = devices; + self.model.devices = devices; } Event::DeviceFailed(path) => { tracing::warn!("Failed operation on device {path}"); - if let Some(device) = self.devices.get_mut(&path) { + if let Some(device) = self.model.devices.get_mut(&path) { if matches!(device.enabled, Active::Disabled | Active::Disabling) { return cosmic::Task::none(); } @@ -248,13 +352,32 @@ impl Page { } Event::SetAdapters(adapters) => { - self.adapters = adapters; - self.update_status(); + let select_adapter = self.model.set_adapters(adapters); - if self.selected_adapter.is_none() && self.adapters.len() == 1 { - return cosmic::task::message(Message::SelectAdapter( - self.adapters.keys().next().cloned(), - )); + if let Some((_, adapter)) = self.model.get_selected_adapter() { + self.heading = fl!( + "bluetooth", + "status", + aliases = format!("“{}”", adapter.alias) + ); + } else { + self.heading = fl!( + "bluetooth", + "status", + aliases = self + .model + .adapters + .values() + .map(|adapter| format!("“{}”", adapter.alias)) + .collect::>() + .into_iter() + .collect::>() + .join(", ") + ); + } + + if let Some(adapter) = select_adapter { + return cosmic::task::message(Message::SelectAdapter(Some(adapter))); } } @@ -266,15 +389,10 @@ impl Page { self.update_status(); if let Some(connection) = self.connection.clone() { - match self.get_selected_adapter_mut() { - Some((path, existing)) - if existing.enabled == Active::Enabled - && existing.scanning == Active::Disabled => - { - existing.scanning = Active::Enabling; - return cosmic::task::future(start_discovery(connection, path)); - } - _ => {} + if let Some(discovery_future) = + self.model.updated_adapter(path, update, connection) + { + return cosmic::task::future(discovery_future); } } else { tracing::warn!("No DBus connection ready"); @@ -282,7 +400,7 @@ impl Page { } Event::UpdatedDevice(path, update) => { - if let Some(existing) = self.devices.get_mut(&path) { + if let Some(existing) = self.model.devices.get_mut(&path) { tracing::debug!("Device {} updated", existing.address); existing.update(update); } @@ -290,26 +408,26 @@ impl Page { Event::RemovedAdapter(path) => { tracing::debug!("Device {path} removed"); - self.adapters.remove(&path); - if self.selected_adapter == Some(path) { - self.selected_adapter = None; + self.model.adapters.remove(&path); + if self.model.selected_adapter == Some(path) { + self.model.selected_adapter = None; } } Event::RemovedDevice(path) => { tracing::debug!("Device {path} removed"); - self.devices.remove(&path); + self.model.devices.remove(&path); } Event::AddedDevice(path, device) => { tracing::debug!("Device {} added", device.address); - self.devices.insert(path, device); + self.model.devices.insert(path, device); } Event::AddedAdapter(path, adapter) => { tracing::debug!("Adapter {} added", adapter.address); - self.adapters.insert(path.clone(), adapter); - if self.selected_adapter.is_none() { + self.model.adapters.insert(path.clone(), adapter); + if self.model.selected_adapter.is_none() { return cosmic::task::message(Message::SelectAdapter(Some(path))); } } @@ -333,7 +451,7 @@ impl Page { passkey, response, } => { - let device = self.devices.get(&device).map_or_else( + let device = self.model.devices.get(&device).map_or_else( || device.to_string(), |device| device.alias_or_addr().to_owned(), ); @@ -376,7 +494,7 @@ impl Page { Message::SetActive(active) => { if let Some(connection) = self.connection.clone() { - if let Some((path, adapter)) = self.get_selected_adapter_mut() { + if let Some((path, adapter)) = self.model.get_selected_adapter_mut() { adapter.enabled = if active { Active::Enabling } else { @@ -398,6 +516,7 @@ impl Page { .chain(Task::done(Message::UpdateStatus.into())); } let tasks: Vec> = self + .model .adapters .iter_mut() .map(|(path, adapter)| { @@ -428,7 +547,7 @@ impl Page { } Message::UpdateStatus => { - self.update_status(); + self.model.update_status(); } Message::DBusConnect(connection) => { @@ -477,24 +596,24 @@ impl Page { } Message::PopupDevice(popup) => { - self.popup_device = popup; + self.model.popup_device = popup; } Message::PopupSetting(popup) => { - self.popup_setting = popup; + self.model.popup_setting = popup; } Message::SelectAdapter(adapter_maybe) => { tracing::debug!("Adapter selected: {adapter_maybe:?}"); - self.selected_adapter = adapter_maybe; - self.update_status(); + self.model.selected_adapter = adapter_maybe; + self.model.update_status(); let Some(connection) = self.connection.as_ref() else { tracing::error!("No DBus connection ready"); return Task::none(); }; let connection = connection.clone(); - if let Some((path, adapter)) = self.get_selected_adapter_mut() { + if let Some((path, adapter)) = self.model.get_selected_adapter_mut() { let mut fut: Vec> = vec![cosmic::task::future(get_devices( connection.clone(), path.clone(), @@ -512,13 +631,13 @@ impl Page { Message::ForgetDevice(path) => { tracing::debug!("Forgetting to device {path}"); - self.popup_device = None; + self.model.popup_device = None; if self.connection.is_none() { return cosmic::Task::none(); } if let Some(connection) = self.connection.as_ref() { let connection = connection.clone(); - if let Some(device) = self.devices.get_mut(&path) { + if let Some(device) = self.model.devices.get_mut(&path) { device.enabled = Active::Disabling; return cosmic::task::future(forget_device(connection, path.clone())); } @@ -534,7 +653,7 @@ impl Page { } if let Some(connection) = self.connection.as_ref() { let connection = connection.clone(); - if let Some(device) = self.devices.get_mut(&path) { + if let Some(device) = self.model.devices.get_mut(&path) { if matches!(device.enabled, Active::Enabled | Active::Enabling) { return cosmic::Task::none(); } @@ -548,10 +667,10 @@ impl Page { Message::DisconnectDevice(path) => { tracing::debug!("Disconnecting device {path}"); - self.popup_device = None; + self.model.popup_device = None; if let Some(connection) = self.connection.as_ref() { let connection = connection.clone(); - if let Some(device) = self.devices.get_mut(&path) { + if let Some(device) = self.model.devices.get_mut(&path) { if matches!(device.enabled, Active::Disabled | Active::Disabling) { return cosmic::Task::none(); } @@ -600,73 +719,6 @@ impl Page { cosmic::Task::none() } - - fn update_status(&mut self) { - if let Some((_, adapter)) = self.get_selected_adapter() { - self.heading = fl!( - "bluetooth", - "status", - aliases = format!("“{}”", adapter.alias) - ); - } else { - self.heading = fl!( - "bluetooth", - "status", - aliases = self - .adapters - .values() - .map(|adapter| format!("“{}”", adapter.alias)) - .collect::>() - .into_iter() - .collect::>() - .join(", ") - ); - } - self.active = if let Some((_, adapter)) = self.get_selected_adapter() { - adapter.enabled - } else if self - .adapters - .values() - .any(|adapter| matches!(adapter.enabled, Active::Enabled | Active::Enabling)) - { - Active::Enabled - } else { - Active::Disabled - } - } - fn adapter_connected(&self, adapter_path: &OwnedObjectPath) -> bool { - self.devices - .iter() - .any(|(path, device)| path.starts_with(adapter_path.as_str()) && device.is_connected()) - } - fn get_selected_adapter(&self) -> Option<(&'_ OwnedObjectPath, &'_ Adapter)> { - if let Some(iface) = &self.selected_adapter { - self.adapters.get_key_value(iface) - } else { - None - } - } - fn devices_for_adapter<'a>( - &'a self, - adapter_path: &'a OwnedObjectPath, - ) -> impl Iterator { - self.devices.iter().filter_map(|(path, device)| { - if device.adapter.eq(adapter_path) { - Some((path, device)) - } else { - None - } - }) - } - fn get_selected_adapter_mut(&mut self) -> Option<(OwnedObjectPath, &'_ mut Adapter)> { - if let Some(path) = &self.selected_adapter { - self.adapters - .get_mut(path) - .map(|adapter| (path.to_owned(), adapter)) - } else { - None - } - } } fn status() -> Section { @@ -708,8 +760,9 @@ fn status() -> Section { } let status = page + .model .get_selected_adapter() - .map_or(page.active, |(_, adapter)| adapter.enabled); + .map_or(page.model.active, |(_, adapter)| adapter.enabled); let mut bluetooth_toggle = settings::item::builder(&descriptions[bluetooth]); if matches!(status, Active::Enabling | Active::Enabled) { @@ -754,23 +807,26 @@ fn connected_devices() -> Section { .title(fl!("bluetooth-paired")) .descriptions(descriptions) .show_while::(|page| { - page.selected_adapter.as_ref().map(|adapter| { - page.devices_for_adapter(adapter) + page.model.selected_adapter.as_ref().map(|adapter| { + page.model + .devices_for_adapter(adapter) .any(|(_, device)| device.paired) }) == Some(true) - && page.active != Active::Disabled + && page.model.active != Active::Disabled }) .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; let section = settings::section().title(§ion.title); - page.devices_for_adapter(page.selected_adapter.as_ref().unwrap()) + page.model + .devices_for_adapter(page.model.selected_adapter.as_ref().unwrap()) .filter_map(|(path, device)| { if !device.paired { return None; } let device_menu: Element<_> = if page + .model .popup_device .as_deref() .map_or(false, |p| path.as_str() == p.as_str()) @@ -854,17 +910,19 @@ fn available_devices() -> Section { .title(fl!("bluetooth-available")) .descriptions(descriptions) .show_while::(|page| { - page.selected_adapter.as_ref().map(|adapter| { - page.devices_for_adapter(adapter) + page.model.selected_adapter.as_ref().map(|adapter| { + page.model + .devices_for_adapter(adapter) .any(|(_, device)| !device.paired) }) == Some(true) - && page.active != Active::Disabled + && page.model.active != Active::Disabled }) .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; let section = settings::section().title(§ion.title); - page.devices_for_adapter(page.selected_adapter.as_ref().unwrap()) + page.model + .devices_for_adapter(page.model.selected_adapter.as_ref().unwrap()) .filter_map(|(path, device)| { if device.paired { return None::>; @@ -907,12 +965,15 @@ fn multiple_adapter() -> Section { Section::default() .title(fl!("bluetooth-adapters")) .descriptions(descriptions) - .show_while::(|page| page.adapters.len() > 1 && page.selected_adapter.is_none()) + .show_while::(|page| { + page.model.adapters.len() > 1 && page.model.selected_adapter.is_none() + }) .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; let section = settings::section().title(§ion.title); - page.adapters + page.model + .adapters .iter() .map(|(path, adapter)| { let mut items = vec![