Merge tag 'epoch-1.7.0' into local/yoda-main-1.3.0

Epoch 1.7.0

# Conflicts:
#	Cargo.lock
This commit is contained in:
Lionel DARNIS 2026-08-27 07:52:54 +02:00
commit 29ed971e64
38 changed files with 989 additions and 651 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings"
version = "1.5.0"
version = "1.7.0"
edition = "2024"
license = "GPL-3.0-only"
publish = false

View file

@ -4,6 +4,7 @@ use cosmic::iced::stream;
use cosmic::theme::CosmicTheme;
use cosmic::widget::{dropdown, settings, text};
use cosmic::{Apply, Element, Task, surface};
use cosmic_comp_config::CosmicCompConfig;
pub use cosmic_comp_config::ZoomMovement;
use cosmic_config::CosmicConfigEntry;
use cosmic_settings_a11y_manager_subscription as cosmic_a11y_manager;
@ -36,11 +37,21 @@ pub struct Page {
daemon_helper: cosmic_config::Config,
dbus_sender: Option<UnboundedSender<a11y_bus::Request>>,
reader_enabled: bool,
comp_config: CosmicCompConfig,
comp_helper: cosmic_config::Config,
}
impl Default for Page {
fn default() -> Self {
let daemon_helper = CosmicSettingsDaemonConfig::config().unwrap();
let comp_helper = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
let comp_config =
CosmicCompConfig::get_entry(&comp_helper).unwrap_or_else(|(errs, config)| {
for err in errs {
tracing::warn!(?err, "error while reading cosmic-comp config");
}
config
});
Page {
dbus_sender: None,
entity: page::Entity::default(),
@ -65,6 +76,8 @@ impl Default for Page {
.unwrap_or_default(),
daemon_helper,
reader_enabled: false,
comp_config,
comp_helper,
}
}
}
@ -77,6 +90,7 @@ pub enum Message {
ProtocolUnavailable,
Return,
ScreenReaderEnabled(bool),
SetCursorShakeToFind(bool),
SetScreenFilterActive(bool),
SetScreenFilterSelection(ColorFilter),
SetScreenInverted(bool),
@ -187,6 +201,8 @@ pub fn vision() -> section::Section<crate::pages::Message> {
invert_colors = fl!("accessibility", "invert-colors");
color_filters = fl!("accessibility", "color-filters");
color_filter_type = fl!("color-filter");
shake_cursor = fl!("accessibility", "shake-cursor");
shake_cursor_desc = fl!("accessibility", "shake-cursor-desc");
});
Section::default()
@ -276,6 +292,13 @@ pub fn vision() -> section::Section<crate::pages::Message> {
settings::item::builder(&descriptions[color_filter_type]).control(dropdown)
})
.add(
settings::item::builder(&descriptions[shake_cursor])
.description(&descriptions[shake_cursor_desc])
.toggler(page.comp_config.cursor_shake_to_find, |enabled| {
Message::SetCursorShakeToFind(enabled).into()
}),
)
.into()
})
}
@ -416,6 +439,14 @@ impl Page {
Message::Surface(a) => {
return cosmic::task::message(crate::app::Message::Surface(a));
}
Message::SetCursorShakeToFind(enabled) => {
if let Err(err) = self
.comp_config
.set_cursor_shake_to_find(&self.comp_helper, enabled)
{
tracing::error!(?err, "failed to set cursor_shake_to_find");
}
}
Message::SetSoundMono(active) => {
if let Err(err) = self
.daemon_config

View file

@ -12,7 +12,7 @@ use futures::channel::oneshot;
use futures::{SinkExt, StreamExt};
use slotmap::SlotMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use zbus::zvariant::OwnedObjectPath;
@ -20,6 +20,8 @@ use zbus::zvariant::OwnedObjectPath;
use crate::service_manager::MockServiceManager;
use crate::service_manager::ServiceManagerHandle;
static BLUETOOTH_PAGE_LABEL: LazyLock<String> = LazyLock::new(|| fl!("bluetooth"));
enum Dialog {
RequestConfirmation {
device: String,
@ -173,6 +175,19 @@ impl page::Page<crate::pages::Message> for Page {
.description(fl!("xdg-entry-bluetooth-comment"))
}
fn header(&self) -> Option<Element<'_, crate::pages::Message>> {
if self.model.adapters.len() > 1 {
let (_, adapter) = self.model.get_selected_adapter()?;
return Some(crate::widget::sub_page_header(
&adapter.alias,
BLUETOOTH_PAGE_LABEL.as_str(),
Message::SelectAdapter(None).into(),
));
}
None
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
@ -392,6 +407,30 @@ impl From<Event> for Message {
}
impl Page {
fn update_heading(&mut self) {
self.heading = if let Some((_, adapter)) = self.model.get_selected_adapter() {
fl!(
"bluetooth",
"status",
aliases = format!("“{}”", adapter.alias)
)
} else {
fl!(
"bluetooth",
"status",
aliases = self
.model
.adapters
.values()
.map(|adapter| format!("“{}”", adapter.alias))
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>()
.join(", ")
)
};
}
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::Message> {
let span = tracing::span!(tracing::Level::INFO, "bluetooth::update");
let _span = span.enter();
@ -437,28 +476,7 @@ impl Page {
Event::SetAdapters(adapters) => {
let select_adapter = self.model.set_adapters(adapters);
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::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>()
.join(", ")
);
}
self.update_heading();
if let Some(adapter) = select_adapter {
return cosmic::task::message(Message::SelectAdapter(Some(adapter)));
@ -741,6 +759,7 @@ impl Page {
Message::SelectAdapter(adapter_maybe) => {
tracing::debug!("Adapter selected: {adapter_maybe:?}");
self.model.selected_adapter = adapter_maybe;
self.update_heading();
self.model.update_status();
let Some(connection) = self.connection.as_ref() else {
tracing::error!("No DBus connection ready");
@ -1158,6 +1177,51 @@ impl Page {
#[cfg(test)]
mod tests {
use super::*;
use cosmic_settings_page::Page as _;
fn adapter(path: &str, alias: &str) -> (OwnedObjectPath, Adapter) {
(
OwnedObjectPath::try_from(path).unwrap(),
Adapter {
alias: alias.to_owned(),
..Adapter::default()
},
)
}
fn page_with_two_adapters() -> Page {
let mut page = Page::default();
let adapters = [
adapter("/org/bluez/hci0", "Adapter 0"),
adapter("/org/bluez/hci1", "Adapter 1"),
]
.into_iter()
.collect();
let _task = page.update(Message::BluetoothEvent(Event::SetAdapters(adapters)));
page
}
#[test]
fn bluetooth_header_is_absent_at_adapter_chooser() {
let page = page_with_two_adapters();
assert!(page.header().is_none());
}
#[test]
fn bluetooth_header_tracks_adapter_selection_and_clearing() {
let mut page = page_with_two_adapters();
let selected = OwnedObjectPath::try_from("/org/bluez/hci1").unwrap();
let _task = page.update(Message::SelectAdapter(Some(selected)));
assert!(page.header().is_some());
assert!(page.heading.contains("Adapter 1"));
let _task = page.update(Message::SelectAdapter(None));
assert!(page.header().is_none());
assert!(page.heading.contains("Adapter 0"));
assert!(page.heading.contains("Adapter 1"));
}
#[test]
fn test_dbus_service_unknown_with_installed_service_queries_manager() {

View file

@ -1103,6 +1103,12 @@ impl Page {
.arg(itoa::Buffer::new().format(x))
.arg("--pos-y")
.arg(itoa::Buffer::new().format(y))
.arg("--refresh")
.arg(format!(
"{}.{:03}",
current.refresh_rate / 1000,
current.refresh_rate % 1000
))
.arg(name)
.arg(itoa::Buffer::new().format(current.size.0))
.arg(itoa::Buffer::new().format(current.size.1));

View file

@ -118,8 +118,8 @@ pub enum SourceContext {
MoveDown(DefaultKey),
MoveUp(DefaultKey),
Remove(DefaultKey),
Settings(DefaultKey),
ViewLayout(DefaultKey),
// Settings(DefaultKey),
// ViewLayout(DefaultKey),
}
pub type Locale = String;
@ -211,6 +211,7 @@ fn popover_menu_row(
id: DefaultKey,
label: String,
message: impl Fn(DefaultKey) -> SourceContext + 'static,
disabled: bool,
) -> cosmic::Element<'static, Message> {
let spacing = theme::spacing();
widget::text::body(label)
@ -219,37 +220,54 @@ fn popover_menu_row(
.padding([spacing.space_xxxs, spacing.space_xs])
.width(Length::Fill)
.class(theme::Button::MenuItem)
.on_press(Message::SourceContext(message(id)))
.on_press_maybe(if disabled {
None
} else {
Some(Message::SourceContext(message(id)))
})
.apply(Element::from)
}
fn popover_menu(id: DefaultKey) -> cosmic::Element<'static, Message> {
fn popover_menu(
id: DefaultKey,
is_first: bool,
is_last: bool,
) -> cosmic::Element<'static, Message> {
widget::column::with_children([
popover_menu_row(
id,
fl!("keyboard-sources", "move-up"),
SourceContext::MoveUp,
is_first,
),
popover_menu_row(
id,
fl!("keyboard-sources", "move-down"),
SourceContext::MoveDown,
is_last,
),
widget::divider::horizontal::default()
.apply(widget::container)
.padding(8)
.into(),
// popover_menu_row(
// id,
// fl!("keyboard-sources", "settings"),
// SourceContext::Settings,
// false,
// ),
// popover_menu_row(
// id,
// fl!("keyboard-sources", "view-layout"),
// SourceContext::ViewLayout,
// false,
// ),
popover_menu_row(
id,
fl!("keyboard-sources", "settings"),
SourceContext::Settings,
fl!("keyboard-sources", "remove"),
SourceContext::Remove,
false,
),
popover_menu_row(
id,
fl!("keyboard-sources", "view-layout"),
SourceContext::ViewLayout,
),
popover_menu_row(id, fl!("keyboard-sources", "remove"), SourceContext::Remove),
])
.width(Length::Fixed(200.0))
.apply(widget::container)
@ -258,7 +276,12 @@ fn popover_menu(id: DefaultKey) -> cosmic::Element<'static, Message> {
.into()
}
fn popover_button(id: DefaultKey, expanded: bool) -> cosmic::Element<'static, Message> {
fn popover_button(
id: DefaultKey,
expanded: bool,
is_first: bool,
is_last: bool,
) -> cosmic::Element<'static, Message> {
let on_press = Message::ExpandInputSourcePopover(if expanded { None } else { Some(id) });
let button = button::icon(icon::from_name("view-more-symbolic"))
@ -268,7 +291,7 @@ fn popover_button(id: DefaultKey, expanded: bool) -> cosmic::Element<'static, Me
if expanded {
widget::popover(button)
.position(widget::popover::Position::Bottom)
.popup(popover_menu(id))
.popup(popover_menu(id, is_first, is_last))
.on_close(Message::ExpandInputSourcePopover(None))
.into()
} else {
@ -280,10 +303,12 @@ fn input_source(
id: DefaultKey,
description: &str,
expanded_source_popover: Option<DefaultKey>,
is_first: bool,
is_last: bool,
) -> cosmic::Element<'_, Message> {
let expanded = expanded_source_popover.is_some_and(|expanded_id| expanded_id == id);
settings::item(description, popover_button(id, expanded)).into()
settings::item(description, popover_button(id, expanded, is_first, is_last)).into()
}
fn special_char_radio_row<'a>(
@ -521,15 +546,13 @@ impl Page {
let _removed = self.active_layouts.remove(pos);
self.update_xkb_config();
}
}
} // SourceContext::Settings(_id) => {
// eprintln!("settings not implemented");
// }
SourceContext::Settings(_id) => {
eprintln!("settings not implemented");
}
SourceContext::ViewLayout(_id) => {
eprintln!("view layout not implemented");
}
// SourceContext::ViewLayout(_id) => {
// eprintln!("view layout not implemented");
// }
}
}
@ -747,12 +770,17 @@ fn input_sources() -> Section<crate::pages::Message> {
// TODO Need something more custom, with drag and drop
let mut section = settings::section().title(&section.title);
for id in &page.active_layouts {
for (position, id) in page.active_layouts.iter().enumerate() {
if let Some((_locale, _variant, description, _source)) =
page.keyboard_layouts.get(*id)
{
section =
section.add(input_source(*id, description, page.expanded_source_popover));
section = section.add(input_source(
*id,
description,
page.expanded_source_popover,
position == 0,
position + 1 == page.active_layouts.len(),
));
}
}