feat(sound): redesign with separate device profiles page (#1500)

This commit is contained in:
Michael Murphy 2025-11-25 21:46:17 +01:00 committed by GitHub
parent 6ebc2208ed
commit 2c9f60cd5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 3179 additions and 1971 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
license = "GPL-3.0-only"
publish = false
@ -72,7 +72,7 @@ tachyonix = "0.3.1"
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
tokio = { workspace = true, features = ["fs", "io-util", "sync"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.20"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
udev = { version = "0.9.3", optional = true }
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
bluez-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }

View file

@ -550,6 +550,13 @@ impl cosmic::Application for SettingsApp {
}
}
#[cfg(feature = "page-sound")]
crate::pages::Message::SoundDeviceProfiles(message) => {
if let Some(page) = self.pages.page_mut::<sound::device_profiles::Page>() {
return page.update(message).map(Into::into);
}
}
crate::pages::Message::StartupApps(message) => {
if let Some(page) = self.pages.page_mut::<applications::startup_apps::Page>() {
return page.update(message).map(Into::into);

View file

@ -201,11 +201,6 @@ fn init_localizer() {
}
fn init_logger() {
let log_level = std::env::var("RUST_LOG")
.ok()
.and_then(|level| level.parse::<tracing::Level>().ok())
.unwrap_or(tracing::Level::INFO);
let log_format = tracing_subscriber::fmt::format()
.pretty()
.without_time()
@ -214,17 +209,14 @@ fn init_logger() {
.with_target(false)
.with_thread_names(true);
let log_filter = tracing_subscriber::fmt::Layer::default()
let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format)
.with_filter(tracing_subscriber::filter::filter_fn(move |metadata| {
let target = metadata.target();
metadata.level() == &tracing::Level::ERROR
|| ((target.starts_with("cosmic_settings") || target.starts_with("cosmic_bg"))
&& metadata.level() <= &log_level)
}));
.event_format(log_format);
tracing_subscriber::registry().with(log_filter).init();
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
.with(log_layer)
.init();
}
#[macro_export]

View file

@ -84,6 +84,8 @@ pub enum Message {
Region(time::region::Message),
#[cfg(feature = "page-sound")]
Sound(sound::Message),
#[cfg(feature = "page-sound")]
SoundDeviceProfiles(sound::device_profiles::Message),
StartupApps(applications::startup_apps::Message),
#[cfg(feature = "page-users")]
User(system::users::Message),

View file

@ -0,0 +1,96 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{Apply, widget};
use cosmic_settings_page::{self as page, Section, section};
use cosmic_settings_sound_subscription::{self as subscription};
use slotmap::SlotMap;
#[derive(Clone, Debug)]
pub enum Message {}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::SoundDeviceProfiles(message)
}
}
impl From<Message> for crate::Message {
fn from(message: Message) -> Self {
crate::Message::PageMessage(message.into())
}
}
#[derive(Default)]
pub struct Page {
entity: page::Entity,
}
impl page::AutoBind<crate::pages::Message> for Page {}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("sound-device-profiles", "preferences-sound-symbolic")
.title(fl!("sound-device-profiles"))
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(view())])
}
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
cosmic::Task::done(crate::pages::Message::Sound(super::Message::Reload))
}
fn set_id(&mut self, entity: cosmic_settings_page::Entity) {
self.entity = entity;
}
fn subscription(
&self,
_core: &cosmic::Core,
) -> cosmic::iced::Subscription<crate::pages::Message> {
cosmic::iced::Subscription::run(subscription::watch)
.map(|message| super::Message::Subscription(message).into())
}
}
impl Page {
pub fn update(&mut self, _message: Message) -> cosmic::Task<crate::app::Message> {
cosmic::Task::none()
}
}
pub fn view() -> Section<crate::pages::Message> {
Section::default().view::<Page>(move |binder, _page, _section| {
let sound_page_id = binder.find_page_by_id("sound").unwrap().0;
let sound_page = binder.page[sound_page_id]
.downcast_ref::<super::Page>()
.unwrap();
let devices = sound_page
.model
.device_profile_dropdowns
.iter()
.cloned()
.map(|(device_id, name, active_profile, indexes, descriptions)| {
let dropdown = widget::dropdown::popup_dropdown(
descriptions,
active_profile,
move |id| super::Message::SetProfile(device_id, indexes[id]),
cosmic::iced::window::Id::RESERVED,
super::Message::Surface,
crate::Message::from,
)
.apply(cosmic::Element::from)
.map(crate::pages::Message::from);
widget::settings::item::builder(name).control(dropdown)
});
widget::settings::section().extend(devices).into()
})
}

View file

@ -1,6 +1,8 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod device_profiles;
use cosmic::{
Apply, Element, Task,
iced::{Alignment, Length, window},
@ -9,43 +11,42 @@ use cosmic::{
};
use cosmic_config::{Config, ConfigGet, ConfigSet};
use cosmic_settings_page::{self as page, Section, section};
use cosmic_settings_sound_subscription as subscription;
use slab::Slab;
use slotmap::SlotMap;
use cosmic_settings_sound_subscription as subscription;
const AUDIO_CONFIG: &str = "com.system76.CosmicAudio";
const AMPLIFICATION_SINK: &str = "amplification_sink";
const AMPLIFICATION_SOURCE: &str = "amplification_source";
#[derive(Clone, Debug)]
pub enum Message {
/// Change the balance of the active sink.
SinkBalanceChanged(u32),
/// Reload the model
Reload,
/// Change the default output.
SinkChanged(usize),
/// Toggle the mute status of the output.
SinkMuteToggle,
/// Change the active profile for an output.
SinkProfileChanged(usize),
/// Request to change the default output volume.
SinkVolumeChanged(u32),
/// Toggle amplification for sink
ToggleOverAmplificationSink(bool),
SetDefaultSink(usize),
/// Change the default input output.
SourceChanged(usize),
/// Toggle the mute status of the input output.
SourceMuteToggle,
/// Change the active profile for an output.
SourceProfileChanged(usize),
SetDefaultSource(usize),
/// Set the profile of a sound device.
SetProfile(u32, u32),
/// Change the balance of the active sink.
SetSinkBalance(u32),
/// Request to change the default output volume.
SetSinkVolume(u32),
/// Request to change the input volume.
SourceVolumeChanged(u32),
/// Toggle amplification for sink
ToggleOverAmplificationSource(bool),
SetSourceVolume(u32),
/// Messages handled by the sound module in cosmic-settings-subscriptions
Subscription(subscription::Message),
/// Surface Action
Surface(surface::Action),
/// Toggle the mute status of the output.
ToggleSinkMute,
/// Toggle the mute status of the input output.
ToggleSourceMute,
/// Toggle amplification for sink
ToggleOverAmplificationSink(bool),
/// Toggle amplification for sink
ToggleOverAmplificationSource(bool),
}
impl From<Message> for crate::pages::Message {
@ -66,15 +67,32 @@ impl From<subscription::Message> for Message {
}
}
#[derive(Default)]
pub struct Page {
entity: page::Entity,
model: subscription::Model,
device_profiles: page::Entity,
pub(self) model: subscription::Model,
sound_config: Option<Config>,
amplification_sink: bool,
amplification_source: bool,
}
impl Default for Page {
fn default() -> Self {
let mut model = subscription::Model::default();
model.unplugged_text = fl!("sound-device-port-unplugged");
model.hd_audio_text = fl!("sound-hd-audio");
model.usb_audio_text = fl!("sound-usb-audio");
Self {
entity: page::Entity::default(),
device_profiles: page::Entity::default(),
model,
sound_config: None,
amplification_sink: false,
amplification_source: false,
}
}
}
impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
match Config::new(AUDIO_CONFIG, 1) {
@ -97,7 +115,11 @@ impl page::Page<crate::pages::Message> for Page {
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(output()), sections.insert(input())])
Some(vec![
sections.insert(output()),
sections.insert(input()),
sections.insert(device_profiles()),
])
}
fn info(&self) -> page::Info {
@ -106,6 +128,10 @@ impl page::Page<crate::pages::Message> for Page {
.description(fl!("sound", "desc"))
}
fn set_id(&mut self, entity: page::Entity) {
self.entity = entity;
}
fn subscription(
&self,
_core: &cosmic::Core,
@ -115,10 +141,9 @@ impl page::Page<crate::pages::Message> for Page {
}
fn on_leave(&mut self) -> Task<crate::pages::Message> {
self.model.clear();
*self = Page {
entity: self.entity,
device_profiles: self.device_profiles,
..Page::default()
};
@ -126,37 +151,65 @@ impl page::Page<crate::pages::Message> for Page {
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(
mut page: page::Insert<crate::pages::Message>,
) -> page::Insert<crate::pages::Message> {
let id = page.sub_page_with_id::<device_profiles::Page>();
let model = page.model.page_mut::<Page>().unwrap();
model.device_profiles = id;
page
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Task<crate::app::Message> {
match message {
Message::SinkBalanceChanged(balance) => {
Message::Surface(a) => return cosmic::task::message(crate::app::Message::Surface(a)),
Message::Subscription(message) => {
return self
.model
.sink_balance_changed(balance)
.map(|message| Message::Subscription(message).into());
}
Message::SinkChanged(pos) => {
return self
.model
.sink_changed(pos)
.update(message)
.map(|message| Message::Subscription(message).into());
}
Message::SinkMuteToggle => self.model.sink_mute_toggle(),
Message::SinkProfileChanged(profile) => {
Message::SetSinkBalance(balance) => {
return self
.model
.sink_profile_changed(profile)
.set_sink_balance(balance)
.map(|message| Message::Subscription(message).into());
}
Message::SinkVolumeChanged(volume) => {
Message::SetDefaultSink(pos) => {
return self
.model
.sink_volume_changed(volume)
.set_default_sink(pos)
.map(|message| Message::Subscription(message).into());
}
Message::SetDefaultSource(pos) => {
return self
.model
.set_default_source(pos)
.map(|message| Message::Subscription(message).into());
}
Message::ToggleSinkMute => self.model.toggle_sink_mute(),
Message::ToggleSourceMute => self.model.toggle_source_mute(),
Message::SetSinkVolume(volume) => {
return self
.model
.set_sink_volume(volume)
.map(|message| Message::Subscription(message).into());
}
Message::SetSourceVolume(volume) => {
return self
.model
.set_source_volume(volume)
.map(|message| Message::Subscription(message).into());
}
@ -170,29 +223,6 @@ impl Page {
}
}
Message::SourceChanged(pos) => {
return self
.model
.source_changed(pos)
.map(|message| Message::Subscription(message).into());
}
Message::SourceMuteToggle => self.model.source_mute_toggle(),
Message::SourceProfileChanged(profile) => {
return self
.model
.source_profile_changed(profile)
.map(|message| Message::Subscription(message).into());
}
Message::SourceVolumeChanged(volume) => {
return self
.model
.source_volume_changed(volume)
.map(|message| Message::Subscription(message).into());
}
Message::ToggleOverAmplificationSource(enabled) => {
self.amplification_source = enabled;
@ -203,14 +233,17 @@ impl Page {
}
}
Message::Subscription(message) => {
return self
.model
.update(message)
.map(|message| Message::Subscription(message).into());
Message::SetProfile(object_id, index) => {
self.model.set_profile(object_id, index, true);
}
Message::Surface(a) => return cosmic::task::message(crate::app::Message::Surface(a)),
Message::Reload => {
let mut model = subscription::Model::default();
model.hd_audio_text = std::mem::take(&mut self.model.hd_audio_text);
model.unplugged_text = std::mem::take(&mut self.model.unplugged_text);
model.usb_audio_text = std::mem::take(&mut self.model.usb_audio_text);
self.model = model;
}
}
Task::none()
@ -223,7 +256,6 @@ fn input() -> Section<crate::pages::Message> {
let volume = descriptions.insert(fl!("sound-input", "volume"));
let device = descriptions.insert(fl!("sound-input", "device"));
let _level = descriptions.insert(fl!("sound-input", "level"));
let profile = descriptions.insert(fl!("profile"));
let amplification = descriptions.insert(fl!("amplification"));
let amplification_desc = descriptions.insert(fl!("amplification", "desc"));
@ -237,12 +269,12 @@ fn input() -> Section<crate::pages::Message> {
let slider = if page.amplification_source {
widget::slider(0..=150, page.model.source_volume, |change| {
Message::SourceVolumeChanged(change).into()
Message::SetSourceVolume(change).into()
})
.breakpoints(&[100])
} else {
widget::slider(0..=100, page.model.source_volume, |change| {
Message::SourceVolumeChanged(change).into()
Message::SetSourceVolume(change).into()
})
};
@ -254,7 +286,7 @@ fn input() -> Section<crate::pages::Message> {
} else {
"audio-input-microphone-symbolic"
}))
.on_press(Message::SourceMuteToggle.into()),
.on_press(Message::ToggleSourceMute.into()),
)
.push(
widget::text::body(&page.model.source_volume_text)
@ -266,7 +298,7 @@ fn input() -> Section<crate::pages::Message> {
let devices = widget::dropdown::popup_dropdown(
page.model.sources(),
Some(page.model.active_source().unwrap_or(0)),
Message::SourceChanged,
Message::SetDefaultSource,
window::Id::RESERVED,
Message::Surface,
crate::Message::from,
@ -282,21 +314,6 @@ fn input() -> Section<crate::pages::Message> {
))
.add(settings::item(&*section.descriptions[device], devices));
if !page.model.source_profiles().is_empty() {
let dropdown = widget::dropdown::popup_dropdown(
page.model.source_profiles(),
page.model.active_source_profile(),
Message::SourceProfileChanged,
window::Id::RESERVED,
Message::Surface,
crate::Message::from,
)
.apply(Element::from)
.map(crate::pages::Message::from);
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
}
controls = controls.add(
settings::item::builder(&*section.descriptions[amplification])
.description(&*section.descriptions[amplification_desc])
@ -316,7 +333,6 @@ fn output() -> Section<crate::pages::Message> {
let volume = descriptions.insert(fl!("sound-output", "volume"));
let device = descriptions.insert(fl!("sound-output", "device"));
let _level = descriptions.insert(fl!("sound-output", "level"));
let profile = descriptions.insert(fl!("profile"));
let balance = descriptions.insert(fl!("sound-output", "balance"));
let left = descriptions.insert(fl!("sound-output", "left"));
let right = descriptions.insert(fl!("sound-output", "right"));
@ -330,12 +346,12 @@ fn output() -> Section<crate::pages::Message> {
.view::<Page>(move |_binder, page, section| {
let slider = if page.amplification_sink {
widget::slider(0..=150, page.model.sink_volume, |change| {
Message::SinkVolumeChanged(change).into()
Message::SetSinkVolume(change).into()
})
.breakpoints(&[100])
} else {
widget::slider(0..=100, page.model.sink_volume, |change| {
Message::SinkVolumeChanged(change).into()
Message::SetSinkVolume(change).into()
})
};
@ -347,7 +363,7 @@ fn output() -> Section<crate::pages::Message> {
} else {
widget::icon::from_name("audio-volume-high-symbolic")
})
.on_press(Message::SinkMuteToggle.into()),
.on_press(Message::ToggleSinkMute.into()),
)
.push(
widget::text::body(&page.model.sink_volume_text)
@ -360,7 +376,7 @@ fn output() -> Section<crate::pages::Message> {
let devices = widget::dropdown::popup_dropdown(
page.model.sinks(),
Some(page.model.active_sink().unwrap_or(0)),
Message::SinkChanged,
Message::SetDefaultSink,
window::Id::RESERVED,
Message::Surface,
crate::Message::from,
@ -374,24 +390,8 @@ fn output() -> Section<crate::pages::Message> {
&*section.descriptions[volume],
volume_control,
))
.add(settings::item(&*section.descriptions[device], devices));
if !page.model.sink_profiles().is_empty() {
let dropdown = widget::dropdown::popup_dropdown(
page.model.sink_profiles(),
page.model.active_sink_profile(),
Message::SinkProfileChanged,
window::Id::RESERVED,
Message::Surface,
crate::Message::from,
)
.apply(Element::from)
.map(crate::pages::Message::from);
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
}
if let Some(sink_balance) = page.model.sink_balance {
controls = controls.add(settings::item(
.add(settings::item(&*section.descriptions[device], devices))
.add(settings::item(
&*section.descriptions[balance],
widget::row::with_capacity(4)
.align_y(Alignment::Center)
@ -404,8 +404,9 @@ fn output() -> Section<crate::pages::Message> {
.push(
widget::slider(
0..=200,
((sink_balance + 1.).max(0.) * 100.).round() as u32,
|change| Message::SinkBalanceChanged(change).into(),
(page.model.sink_balance.unwrap_or(1.0).max(0.) * 100.).round()
as u32,
|change| Message::SetSinkBalance(change).into(),
)
.breakpoints(&[100]),
)
@ -416,7 +417,6 @@ fn output() -> Section<crate::pages::Message> {
.align_x(Alignment::Center),
),
));
}
controls = controls.add(
settings::item::builder(&*section.descriptions[amplification])
@ -431,6 +431,34 @@ fn output() -> Section<crate::pages::Message> {
})
}
/// A section for opening the device profiles sub-page.
fn device_profiles() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
button_txt = fl!("sound-device-profiles");
});
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions;
let button = widget::row::with_children(vec![
widget::horizontal_space().into(),
widget::icon::from_name("go-next-symbolic").size(16).into(),
]);
let device_profiles = settings::item::builder(&*descriptions[button_txt])
.control(button)
.spacing(16)
.apply(widget::container)
.class(cosmic::theme::Container::List)
.apply(widget::button::custom)
.class(cosmic::theme::Button::Transparent)
.on_press(crate::pages::Message::Page(page.device_profiles));
settings::section().add(device_profiles).into()
})
}
// fn alerts() -> Section<crate::pages::Message> {
// let mut descriptions = Slab::new();
// let volume = descriptions.insert(fl!("sound-alerts", "volume"));