feat: add Region & Language page

This commit is contained in:
Michael Murphy 2024-11-11 17:25:09 +01:00 committed by GitHub
parent 42a3061da0
commit 8a20cbc748
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1211 additions and 189 deletions

View file

@ -7,7 +7,9 @@ license = "GPL-3.0-only"
[dependencies]
anyhow = "1.0"
as-result = "0.2.1"
ashpd = { version = "0.9", default-features = false, features = ["tokio"], optional = true }
ashpd = { version = "0.9", default-features = false, features = [
"tokio",
], optional = true }
async-channel = "2.3.1"
chrono = "0.4.38"
clap = { version = "4.5.17", features = ["derive"] }
@ -39,6 +41,7 @@ indexmap = "2.5.0"
itertools = "0.13.0"
itoa = "1.0.11"
libcosmic.workspace = true
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
notify = "6.1.1"
once_cell = "1.19.0"
regex = "1.10.6"
@ -63,6 +66,7 @@ xkb-data = "0.2.1"
zbus = { version = "4.4.0", features = ["tokio"], optional = true }
ustr = "1.0.0"
fontdb = "0.16.2"
fixed_decimal = "0.5.6"
[dependencies.cosmic-settings-subscriptions]
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
@ -78,14 +82,14 @@ features = ["experimental", "compiled_data", "icu_datetime_experimental"]
version = "0.15.0"
features = ["fluent-system", "desktop-requester"]
# Contains region-handling logic for Linux
[dependencies.lichen-system]
git = "https://github.com/serpent-os/lichen"
package = "system"
optional = true
[features]
default = [
"a11y",
"dbus-config",
"linux",
"single-instance",
"wgpu",
]
default = ["a11y", "dbus-config", "linux", "single-instance", "wgpu"]
# Default features for Linux
linux = [
@ -95,6 +99,7 @@ linux = [
"page-input",
"page-networking",
"page-power",
"page-region",
"page-sound",
"page-window-management",
"page-workspaces",
@ -106,9 +111,19 @@ linux = [
page-about = ["dep:cosmic-settings-system", "dep:hostname1-zbus", "dep:zbus"]
page-bluetooth = ["dep:bluez-zbus", "dep:zbus"]
page-date = ["dep:timedate-zbus", "dep:zbus"]
page-input = ["dep:cosmic-comp-config", "dep:cosmic-settings-config", "dep:udev"]
page-networking = ["ashpd", "dep:cosmic-dbus-networkmanager", "dep:cosmic-settings-subscriptions", "dep:zbus"]
page-input = [
"dep:cosmic-comp-config",
"dep:cosmic-settings-config",
"dep:udev",
]
page-networking = [
"ashpd",
"dep:cosmic-dbus-networkmanager",
"dep:cosmic-settings-subscriptions",
"dep:zbus",
]
page-power = ["dep:upower_dbus", "dep:zbus"]
page-region = ["dep:lichen-system", "dep:locale1"]
page-sound = ["dep:cosmic-settings-subscriptions"]
page-window-management = ["dep:cosmic-settings-config"]
page-workspaces = ["dep:cosmic-comp-config"]

View file

@ -96,6 +96,7 @@ impl SettingsApp {
PageCommands::Panel => self.pages.page_id::<desktop::panel::Page>(),
#[cfg(feature = "page-power")]
PageCommands::Power => self.pages.page_id::<power::Page>(),
#[cfg(feature = "page-region")]
PageCommands::RegionLanguage => self.pages.page_id::<time::region::Page>(),
#[cfg(feature = "page-sound")]
PageCommands::Sound => self.pages.page_id::<sound::Page>(),
@ -480,6 +481,13 @@ impl cosmic::Application for SettingsApp {
}
}
#[cfg(feature = "page-region")]
crate::pages::Message::Region(message) => {
if let Some(page) = self.pages.page_mut::<time::region::Page>() {
return page.update(message).map(Into::into);
}
}
#[cfg(feature = "page-sound")]
crate::pages::Message::Sound(message) => {
if let Some(page) = self.pages.page_mut::<sound::Page>() {

View file

@ -135,7 +135,8 @@ impl page::Page<crate::pages::Message> for Page {
let cancel_button =
widget::button::standard(fl!("cancel")).on_press(Message::PinCancel);
let dialog = widget::dialog(fl!("bluetooth-confirm-pin"))
let dialog = widget::dialog()
.title(fl!("bluetooth-confirm-pin"))
.control(control)
.primary_action(confirm_button)
.secondary_action(cancel_button)

View file

@ -24,7 +24,7 @@ use cosmic::widget::{
button, color_picker::ColorPickerUpdate, container, flex_row, horizontal_space, radio, row,
scrollable, settings, spin_button, text, ColorPickerModel,
};
use cosmic::{Apply, Element, Task};
use cosmic::{widget, Apply, Element, Task};
#[cfg(feature = "wayland")]
use cosmic_panel_config::CosmicPanelConfig;
use cosmic_settings_page::Section;

View file

@ -308,17 +308,17 @@ impl Page {
);
}
column::with_children(vec![
text_input::search_input(fl!("search-applets"), &self.search)
.on_input(move |s| msg_map(Message::Search(s)))
.on_paste(move |s| msg_map(Message::Search(s)))
.width(Length::Fixed(312.0))
.into(),
list_column.into(),
])
.align_x(Alignment::Center)
.spacing(space_xxs)
.into()
let search = text_input::search_input(fl!("search-applets"), &self.search)
.on_input(move |s| msg_map(Message::Search(s)))
.on_paste(move |s| msg_map(Message::Search(s)))
.width(Length::Fixed(312.0));
column::with_capacity(2)
.push(search)
.push(list_column)
.align_x(Alignment::Center)
.spacing(space_xxs)
.into()
}
#[allow(clippy::too_many_lines)]

View file

@ -344,7 +344,8 @@ impl page::Page<crate::pages::Message> for Page {
/// Task.
fn dialog(&self) -> Option<Element<pages::Message>> {
self.dialog?;
let element = widget::dialog(fl!("dialog", "title"))
let element = widget::dialog()
.title(fl!("dialog", "title"))
.body(fl!("dialog", "change-prompt", time = self.dialog_countdown))
.primary_action(
widget::button::suggested(fl!("dialog", "keep-changes"))

View file

@ -241,7 +241,7 @@ fn input_source(
) -> cosmic::Element<Message> {
let expanded = expanded_source_popover.is_some_and(|expanded_id| expanded_id == id);
settings::flex_item(description, popover_button(id, expanded)).into()
settings::item(description, popover_button(id, expanded)).into()
}
fn special_char_radio_row<'a>(

View file

@ -166,7 +166,8 @@ impl Model {
let secondary_action = button::standard(fl!("cancel"))
.on_press(ShortcutMessage::CancelReplace);
let dialog = widget::dialog(fl!("replace-shortcut-dialog"))
let dialog = widget::dialog()
.title(fl!("replace-shortcut-dialog"))
.icon(icon::from_name("dialog-warning").size(64))
.body(fl!(
"replace-shortcut-dialog",
@ -521,7 +522,7 @@ fn context_drawer(
.into();
let flex_control =
settings::flex_item_row(vec![input, delete_button]).align_items(Alignment::Center);
settings::item_row(vec![input, delete_button]).align_y(Alignment::Center);
section.add(flex_control)
},

View file

@ -321,7 +321,8 @@ impl page::Page<crate::pages::Message> for Page {
let secondary_action = button::standard(fl!("cancel")).on_press(Message::ReplaceCancel);
let dialog = widget::dialog(fl!("replace-shortcut-dialog"))
let dialog = widget::dialog()
.title(fl!("replace-shortcut-dialog"))
.icon(icon::from_name("dialog-warning").size(64))
.body(fl!(
"replace-shortcut-dialog",

View file

@ -63,6 +63,8 @@ pub enum Message {
PanelApplet(desktop::panel::applets_inner::Message),
#[cfg(feature = "page-power")]
Power(power::Message),
#[cfg(feature = "page-region")]
Region(time::region::Message),
#[cfg(feature = "page-sound")]
Sound(sound::Message),
#[cfg(feature = "page-input")]

View file

@ -229,7 +229,8 @@ impl page::Page<crate::pages::Message> for Page {
let primary_action =
widget::button::standard(fl!("ok")).on_press(Message::CancelDialog);
widget::dialog(fl!("vpn-error"))
widget::dialog()
.title(fl!("vpn-error"))
.icon(icon::from_name("dialog-error-symbolic").size(64))
.body(error_kind.localized())
.control(reason)
@ -268,7 +269,8 @@ impl page::Page<crate::pages::Message> for Page {
let secondary_action =
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
widget::dialog(fl!("auth-dialog"))
widget::dialog()
.title(fl!("auth-dialog"))
.icon(icon::from_name("network-vpn-symbolic").size(64))
.body(fl!("auth-dialog", "vpn-description"))
.control(controls)
@ -289,7 +291,8 @@ impl page::Page<crate::pages::Message> for Page {
let secondary_action =
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
widget::dialog(fl!("wireguard-dialog"))
widget::dialog()
.title(fl!("wireguard-dialog"))
.icon(icon::from_name("network-vpn-symbolic").size(64))
.body(fl!("wireguard-dialog", "description"))
.control(input)
@ -306,7 +309,8 @@ impl page::Page<crate::pages::Message> for Page {
let secondary_action =
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
widget::dialog(fl!("remove-connection-dialog"))
widget::dialog()
.title(fl!("remove-connection-dialog"))
.icon(icon::from_name("dialog-information").size(64))
.body(fl!("remove-connection-dialog", "vpn-description"))
.primary_action(primary_action)

View file

@ -158,7 +158,8 @@ impl page::Page<crate::pages::Message> for Page {
let secondary_action =
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
widget::dialog(fl!("auth-dialog"))
widget::dialog()
.title(fl!("auth-dialog"))
.icon(icon::from_name("preferences-wireless-symbolic").size(64))
.body(fl!("auth-dialog", "wifi-description"))
.control(password)
@ -175,7 +176,8 @@ impl page::Page<crate::pages::Message> for Page {
let secondary_action =
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
widget::dialog(fl!("forget-dialog"))
widget::dialog()
.title(fl!("forget-dialog"))
.icon(icon::from_name("dialog-information").size(64))
.body(fl!("forget-dialog", "description"))
.primary_action(primary_action)

View file

@ -131,7 +131,8 @@ impl page::Page<crate::pages::Message> for Page {
let secondary_action =
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
widget::dialog(fl!("remove-connection-dialog"))
widget::dialog()
.title(fl!("remove-connection-dialog"))
.icon(icon::from_name("dialog-information").size(64))
.body(fl!("remove-connection-dialog", "wired-description"))
.primary_action(primary_action)

View file

@ -32,7 +32,12 @@ impl page::AutoBind<crate::pages::Message> for Page {
{
page = page.sub_page::<date::Page>();
}
page = page.sub_page::<region::Page>();
#[cfg(feature = "page-region")]
{
page = page.sub_page::<region::Page>();
}
page
}
}

View file

@ -1,13 +1,117 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use cosmic::iced::{Alignment, Border, Color, Length};
use cosmic::iced_core::text::Wrapping;
use cosmic::widget::{self, button, container};
use cosmic::{theme, Apply, Element};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
use eyre::Context;
use fixed_decimal::FixedDecimal;
use icu::calendar::DateTime;
use icu::datetime::options::components::{self, Bag};
use icu::datetime::options::preferences;
use icu::datetime::DateTimeFormatter;
use icu::decimal::options::FixedDecimalFormatterOptions;
use icu::decimal::FixedDecimalFormatter;
use lichen_system::locale;
use slotmap::{DefaultKey, SlotMap};
use tokio::sync::mpsc;
#[derive(Clone, Debug)]
pub enum Message {
AddLanguage(DefaultKey),
AddLanguageContext,
AddLanguageSearch(String),
SystemLocales(SlotMap<DefaultKey, SystemLocale>),
ExpandLanguagePopover(Option<usize>),
InstallAdditionalLanguages,
SelectRegion(DefaultKey),
SourceContext(SourceContext),
Refresh(Arc<eyre::Result<PageRefresh>>),
RegionContext,
}
impl From<Message> for crate::app::Message {
fn from(message: Message) -> Self {
crate::pages::Message::Region(message).into()
}
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::Region(message)
}
}
enum ContextView {
AddLanguage,
Region,
}
#[derive(Clone, Debug)]
pub enum SourceContext {
MoveDown(usize),
MoveUp(usize),
Remove(usize),
}
#[derive(Clone, Debug)]
pub struct SystemLocale {
lang_code: String,
display_name: String,
region_name: String,
}
impl Eq for SystemLocale {}
impl Ord for SystemLocale {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.display_name.cmp(&other.display_name)
}
}
impl PartialOrd for SystemLocale {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.display_name.partial_cmp(&other.display_name)
}
}
impl PartialEq for SystemLocale {
fn eq(&self, other: &Self) -> bool {
self.display_name == other.display_name
}
}
#[derive(Debug)]
pub struct PageRefresh {
config: Option<(cosmic_config::Config, Vec<String>)>,
registry: Registry,
language: Option<SystemLocale>,
region: Option<SystemLocale>,
available_languages: SlotMap<DefaultKey, SystemLocale>,
system_locales: BTreeMap<String, SystemLocale>,
}
#[derive(Default)]
pub struct Page {
entity: page::Entity,
config: Option<(cosmic_config::Config, Vec<String>)>,
context: Option<ContextView>,
language: Option<SystemLocale>,
region: Option<SystemLocale>,
available_languages: SlotMap<DefaultKey, SystemLocale>,
system_locales: BTreeMap<String, SystemLocale>,
registry: Option<locale::Registry>,
expanded_source_popover: Option<usize>,
add_language_search: String,
}
impl page::Page<crate::pages::Message> for Page {
@ -19,7 +123,10 @@ 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(Section::default())])
Some(vec![
sections.insert(preferred_languages::section()),
sections.insert(formatting::section()),
])
}
fn info(&self) -> page::Info {
@ -27,6 +134,806 @@ impl page::Page<crate::pages::Message> for Page {
.title(fl!("time-region"))
.description(fl!("time-region", "desc"))
}
fn on_enter(
&mut self,
_sender: mpsc::Sender<crate::pages::Message>,
) -> cosmic::Task<crate::pages::Message> {
cosmic::command::future(async move { Message::Refresh(Arc::new(page_reload().await)) })
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
Some(match self.context.as_ref()? {
ContextView::AddLanguage => self.add_language_view(),
ContextView::Region => self.region_view(),
})
}
}
impl Page {
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::app::Message> {
match message {
Message::AddLanguage(id) => {
if let Some(language) = self.available_languages.get(id) {
if let Some((config, locales)) = self.config.as_mut() {
if !locales.contains(&language.lang_code) {
locales.push(language.lang_code.clone());
_ = config.set("system_locales", &locales);
}
}
}
}
Message::SelectRegion(id) => {
let mut commands = Vec::with_capacity(2);
if let Some((region, language)) =
self.available_languages.get(id).zip(self.language.as_ref())
{
self.region = Some(region.clone());
let lang = language.lang_code.clone();
let region = region.lang_code.clone();
commands.push(cosmic::command::future(async move {
_ = set_locale(lang, region).await;
Message::Refresh(Arc::new(page_reload().await))
}));
}
commands.push(cosmic::Task::done(crate::app::Message::CloseContextDrawer));
return cosmic::Task::batch(commands);
}
Message::AddLanguageContext => {
self.context = Some(ContextView::AddLanguage);
return cosmic::Task::done(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("add-language", "context").into(),
));
}
Message::AddLanguageSearch(search) => {
self.add_language_search = search;
}
Message::SystemLocales(languages) => {
self.available_languages = languages;
}
Message::ExpandLanguagePopover(id) => {
self.expanded_source_popover = id;
}
Message::InstallAdditionalLanguages => {
return cosmic::command::future(async move {
_ = tokio::process::Command::new("gnome-language-selector")
.status()
.await;
Message::Refresh(Arc::new(page_reload().await))
})
}
Message::Refresh(result) => match Arc::into_inner(result).unwrap() {
Ok(page_refresh) => {
self.config = page_refresh.config;
self.available_languages = page_refresh.available_languages;
self.system_locales = page_refresh.system_locales;
self.language = page_refresh.language;
self.region = page_refresh.region;
self.registry = Some(page_refresh.registry.0);
}
Err(why) => {
tracing::error!(?why, "failed to get locales from the system");
}
},
Message::RegionContext => {
self.context = Some(ContextView::Region);
return cosmic::Task::done(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("region").into(),
));
}
Message::SourceContext(context_message) => {
self.expanded_source_popover = None;
if let Some((config, locales)) = self.config.as_mut() {
match context_message {
SourceContext::MoveDown(id) => {
if id + 1 < locales.len() {
locales.swap(id, id + 1);
}
}
SourceContext::MoveUp(id) => {
if id > 0 {
locales.swap(id, id - 1);
}
}
SourceContext::Remove(id) => {
let _removed = locales.remove(id);
}
}
_ = config.set("system_locales", &locales);
if let Some(language_code) = locales.get(0) {
if let Some(language) = self
.available_languages
.values()
.find(|lang| &lang.lang_code == language_code)
{
let language = language.clone();
self.language = Some(language.clone());
let region = self.region.clone();
tokio::spawn(async move {
_ = set_locale(
language.lang_code.clone(),
region.unwrap_or(language).lang_code.clone(),
)
.await;
});
}
}
}
}
}
cosmic::Task::none()
}
fn add_language_view(&self) -> cosmic::Element<'_, crate::pages::Message> {
let cosmic::cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing;
let search = widget::search_input(fl!("type-to-search"), &self.add_language_search)
.on_input(Message::AddLanguageSearch)
.on_clear(Message::AddLanguageSearch(String::new()));
let mut list = widget::list_column();
let search_input = &self.add_language_search.trim().to_lowercase();
let svg_accent = Rc::new(|theme: &cosmic::Theme| {
let color = theme.cosmic().accent_color().into();
cosmic::widget::svg::Style { color: Some(color) }
});
for (id, available_language) in &self.available_languages {
if search_input.is_empty()
|| available_language
.display_name
.to_lowercase()
.contains(search_input)
{
let is_installed = self.config.as_ref().map_or(false, |(_, locales)| {
locales.contains(&available_language.lang_code)
});
let button = widget::settings::item_row(vec![
widget::text::body(&available_language.display_name)
.class(if is_installed {
cosmic::theme::Text::Accent
} else {
cosmic::theme::Text::Default
})
.wrapping(Wrapping::Word)
.into(),
widget::horizontal_space().width(Length::Fill).into(),
if is_installed {
widget::icon::from_name("object-select-symbolic")
.size(16)
.icon()
.class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into()
} else {
widget::horizontal_space().width(16).into()
},
])
.apply(widget::container)
.class(cosmic::theme::Container::List)
.apply(widget::button::custom)
.class(cosmic::theme::Button::Transparent)
.on_press_maybe(if is_installed {
None
} else {
Some(Message::AddLanguage(id))
});
list = list.add(button)
}
}
let install_additional_button =
widget::button::standard(fl!("install-additional-languages"))
.on_press(Message::InstallAdditionalLanguages)
.apply(widget::container)
.width(Length::Fill)
.align_x(Alignment::End);
widget::column()
.padding([2, 0])
.spacing(space_l)
.push(search)
.push(list)
.push(install_additional_button)
.apply(Element::from)
.map(crate::pages::Message::Region)
}
fn formatted_date(&self) -> String {
let time_locale = self
.system_locales
.get("LC_TIME")
.or_else(|| self.system_locales.get("LANG"))
.map_or("en_US", |locale| &locale.lang_code)
.split('.')
.next()
.unwrap_or("en_US");
let Ok(locale) = icu::locid::Locale::from_str(time_locale) else {
return String::new();
};
let mut bag = Bag::empty();
bag.day = Some(components::Day::TwoDigitDayOfMonth);
bag.month = Some(components::Month::TwoDigit);
bag.year = Some(components::Year::Numeric);
let options = icu::datetime::DateTimeFormatterOptions::Components(bag);
let dtf = DateTimeFormatter::try_new_experimental(&locale.into(), options).unwrap();
let datetime = DateTime::try_new_gregorian_datetime(2024, 1, 1, 12, 0, 0)
.unwrap()
.to_iso()
.to_any();
dtf.format(&datetime)
.expect("can't format value")
.to_string()
}
fn formatted_dates_and_times(&self) -> String {
let time_locale = self
.system_locales
.get("LC_TIME")
.or_else(|| self.system_locales.get("LANG"))
.map_or("en_US", |locale| &locale.lang_code)
.split('.')
.next()
.unwrap_or("en_US");
let Ok(locale) = icu::locid::Locale::from_str(time_locale) else {
return String::new();
};
let mut bag = Bag::empty();
bag.hour = Some(components::Numeric::Numeric);
bag.minute = Some(components::Numeric::Numeric);
bag.second = Some(components::Numeric::Numeric);
bag.preferences = Some(preferences::Bag::from_hour_cycle(
preferences::HourCycle::H12,
));
// bag.time_zone_name = Some(components::TimeZoneName::ShortSpecific);
bag.day = Some(components::Day::TwoDigitDayOfMonth);
bag.month = Some(components::Month::Short);
bag.year = Some(components::Year::Numeric);
let options = icu::datetime::DateTimeFormatterOptions::Components(bag);
let dtf = DateTimeFormatter::try_new_experimental(&locale.into(), options).unwrap();
let datetime = DateTime::try_new_gregorian_datetime(2024, 1, 1, 12, 0, 0)
.unwrap()
.to_iso()
.to_any();
dtf.format(&datetime)
.expect("can't format value")
.to_string()
}
fn formatted_time(&self) -> String {
let time_locale = self
.system_locales
.get("LC_TIME")
.or_else(|| self.system_locales.get("LANG"))
.map_or("en_US", |locale| &locale.lang_code)
.split('.')
.next()
.unwrap_or("en_US");
let Ok(locale) = icu::locid::Locale::from_str(time_locale) else {
return String::new();
};
let mut bag = Bag::empty();
bag.hour = Some(components::Numeric::Numeric);
bag.minute = Some(components::Numeric::Numeric);
bag.second = Some(components::Numeric::Numeric);
bag.preferences = Some(preferences::Bag::from_hour_cycle(
preferences::HourCycle::H12,
));
let options = icu::datetime::DateTimeFormatterOptions::Components(bag);
let dtf = DateTimeFormatter::try_new_experimental(&locale.into(), options).unwrap();
let datetime = DateTime::try_new_gregorian_datetime(2024, 1, 1, 12, 0, 0)
.unwrap()
.to_iso()
.to_any();
dtf.format(&datetime)
.expect("can't format value")
.to_string()
}
fn formatted_numbers(&self) -> String {
let numerical_locale = self
.system_locales
.get("LC_NUMERIC")
.or_else(|| self.system_locales.get("LANG"))
.map_or("en_US", |locale| &locale.lang_code)
.split('.')
.next()
.unwrap_or("en_US");
let Ok(locale) = icu::locid::Locale::from_str(numerical_locale) else {
return String::new();
};
let options = FixedDecimalFormatterOptions::default();
let formatter = FixedDecimalFormatter::try_new(&locale.into(), options).unwrap();
let mut value = FixedDecimal::from(123456789);
value.multiply_pow10(-2);
formatter.format(&value).to_string()
}
fn region_view(&self) -> cosmic::Element<'_, crate::pages::Message> {
let space_l = theme::active().cosmic().spacing.space_l;
let svg_accent = Rc::new(|theme: &cosmic::Theme| {
let color = theme.cosmic().accent_color().into();
cosmic::widget::svg::Style { color: Some(color) }
});
let search = widget::search_input(fl!("type-to-search"), &self.add_language_search)
.on_input(Message::AddLanguageSearch)
.on_clear(Message::AddLanguageSearch(String::new()));
let mut list = widget::list_column();
let search_input = &self.add_language_search.trim().to_lowercase();
for (id, locale) in &self.available_languages {
if search_input.is_empty() || locale.display_name.to_lowercase().contains(search_input)
{
let is_selected = self
.region
.as_ref()
.map_or(false, |l| l.lang_code == locale.lang_code);
let button = widget::settings::item_row(vec![
widget::text::body(&locale.region_name)
.class(if is_selected {
cosmic::theme::Text::Accent
} else {
cosmic::theme::Text::Default
})
.wrapping(Wrapping::Word)
.into(),
if is_selected {
widget::icon::from_name("object-select-symbolic")
.size(16)
.icon()
.class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into()
} else {
widget::horizontal_space().width(16).into()
},
])
.apply(widget::container)
.class(cosmic::theme::Container::List)
.apply(widget::button::custom)
.class(cosmic::theme::Button::Transparent)
.on_press_maybe(if is_selected {
None
} else {
Some(Message::SelectRegion(id))
});
list = list.add(button)
}
}
widget::column()
.padding([2, 0])
.spacing(space_l)
.push(search)
.push(list)
.apply(Element::from)
.map(crate::pages::Message::Region)
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
mod preferred_languages {
use super::Message;
use cosmic::{
iced::{Alignment, Length},
widget, Apply,
};
use cosmic_settings_page::Section;
pub fn section() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
pref_lang_desc = fl!("preferred-languages", "desc");
add_lang_txt = fl!("add-language");
});
Section::default()
.title(fl!("preferred-languages"))
.descriptions(descriptions)
.view::<super::Page>(move |_binder, page, section| {
let title = widget::text::body(&section.title).font(cosmic::font::bold());
let description = widget::text::body(&section.descriptions[pref_lang_desc]);
let mut content = widget::settings::section();
if let Some(((_config, locales), registry)) =
page.config.as_ref().zip(page.registry.as_ref())
{
for (id, locale) in locales.iter().enumerate() {
if let Some(locale) = registry.locale(locale) {
content = content.add(super::language_element(
id,
locale.display_name.clone(),
page.expanded_source_popover,
));
}
}
}
let add_language_button =
widget::button::standard(&section.descriptions[add_lang_txt])
.on_press(Message::AddLanguageContext)
.apply(widget::container)
.width(Length::Fill)
.align_x(Alignment::End);
widget::column::with_capacity(5)
.push(title)
.push(description)
.push(content)
.push(add_language_button)
.spacing(cosmic::theme::active().cosmic().spacing.space_xxs)
.apply(cosmic::Element::from)
.map(Into::into)
})
}
}
mod formatting {
use super::Message;
use cosmic::{widget, Apply};
use cosmic_settings_page::Section;
pub fn section() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
formatting_txt = fl!("formatting");
dates_txt = fl!("formatting", "dates");
time_txt = fl!("formatting", "time");
date_and_time_txt = fl!("formatting", "date-and-time");
numbers_txt = fl!("formatting", "numbers");
// measurement_txt = fl!("formatting", "measurement");
// paper_txt = fl!("formatting", "paper");
region_txt = fl!("region");
});
let dates_label = [&descriptions[dates_txt], ":"].concat();
let time_label = [&descriptions[time_txt], ":"].concat();
let date_and_time_label = [&descriptions[date_and_time_txt], ":"].concat();
let numbers_label = [&descriptions[numbers_txt], ":"].concat();
// let measurement_label = [&descriptions[measurement_txt], ":"].concat();
// let paper_label = [&descriptions[paper_txt], ":"].concat();
Section::default()
.title(fl!("formatting"))
.descriptions(descriptions)
.view::<super::Page>(move |_binder, page, section| {
let desc = &section.descriptions;
let dates = widget::row::with_capacity(2)
.push(widget::text::body(dates_label.clone()))
.push(widget::text::body(page.formatted_date()).font(cosmic::font::bold()))
.spacing(4);
let time = widget::row::with_capacity(2)
.push(widget::text::body(time_label.clone()))
.push(widget::text::body(page.formatted_time()).font(cosmic::font::bold()))
.spacing(4);
let dates_and_times = widget::row::with_capacity(2)
.push(widget::text::body(date_and_time_label.clone()))
.push(
widget::text::body(page.formatted_dates_and_times())
.font(cosmic::font::bold()),
)
.spacing(4);
let numbers = widget::row::with_capacity(2)
.push(widget::text::body(numbers_label.clone()))
.push(widget::text::body(page.formatted_numbers()).font(cosmic::font::bold()))
.spacing(4);
// TODO: Display measurement and paper demos
// let measurement = widget::row::with_capacity(2)
// .push(widget::text::body(measurement_label.clone()))
// .push(widget::text::body("").font(cosmic::font::bold()))
// .spacing(4);
// let paper = widget::row::with_capacity(2)
// .push(widget::text::body(paper_label.clone()))
// .push(widget::text::body("").font(cosmic::font::bold()))
// .spacing(4);
let formatted_demo = widget::column::with_capacity(6)
.push(dates)
.push(time)
.push(dates_and_times)
.push(numbers)
// .push(measurement)
// .push(paper)
.spacing(4)
.padding(5.0)
.apply(|column| widget::settings::item_row(vec![column.into()]));
let region = page
.region
.as_ref()
.map(|locale| locale.region_name.as_str())
.unwrap_or("");
let select_region = crate::widget::go_next_with_item(
&desc[region_txt],
widget::text::body(region),
Message::RegionContext,
);
widget::settings::section()
.title(&desc[formatting_txt])
.add(formatted_demo)
.add(select_region)
.apply(cosmic::Element::from)
.map(Into::into)
})
}
}
struct Registry(locale::Registry);
impl std::fmt::Debug for Registry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Registry").finish()
}
}
pub async fn page_reload() -> eyre::Result<PageRefresh> {
let conn = zbus::Connection::system()
.await
.wrap_err("zbus system connection error")?;
let registry = locale::Registry::new().wrap_err("failed to get locale registry")?;
let system_locales: BTreeMap<String, SystemLocale> = locale1::locale1Proxy::new(&conn)
.await
.wrap_err("locale1 proxy connect error")?
.locale()
.await
.wrap_err("could not get locale from locale1")?
.into_iter()
.filter_map(|expression| {
let mut fields = expression.split('=');
let var = fields.next()?;
let lang_code = fields.next()?;
let locale = registry.locale(lang_code);
Some((
var.to_owned(),
SystemLocale {
lang_code: lang_code.to_owned(),
display_name: registry
.locale(lang_code)
.map_or(String::from(""), |locale| locale.display_name.clone()),
region_name: locale.map_or(String::from(""), |locale| {
format!(
"{} ({})",
locale.territory.display_name, locale.language.display_name
)
}),
},
))
})
.collect();
let config = cosmic_config::Config::new("com.system76.CosmicSettings", 1)
.ok()
.map(|context| {
let locales = context
.get::<Vec<String>>("system_locales")
.ok()
.unwrap_or_else(|| {
let current = system_locales
.get("LANG")
.map_or("en_US.UTF-8", |l| l.lang_code.as_str())
.to_owned();
vec![current]
});
(context, locales)
});
let language = system_locales
.get("LC_ALL")
.or_else(|| system_locales.get("LANG"))
.cloned();
let region = system_locales
.get("LC_TIME")
.or_else(|| system_locales.get("LANG"))
.cloned();
let mut available_languages_set = BTreeSet::new();
let output = tokio::process::Command::new("localectl")
.arg("list-locales")
.output()
.await
.expect("Failed to run localectl");
let output = String::from_utf8(output.stdout).unwrap_or_default();
for line in output.lines() {
if line == "C.UTF-8" {
continue;
}
if let Some(locale) = registry.locale(line) {
available_languages_set.insert(SystemLocale {
lang_code: line.to_owned(),
display_name: locale.display_name.clone(),
region_name: format!(
"{} ({})",
locale.territory.display_name, locale.language.display_name
),
});
}
}
let mut available_languages = SlotMap::new();
for language in available_languages_set {
available_languages.insert(language);
}
Ok(PageRefresh {
config,
registry: Registry(registry),
language,
region,
available_languages,
system_locales,
})
}
fn language_element(
id: usize,
description: String,
expanded_source_popover: Option<usize>,
) -> cosmic::Element<'static, Message> {
let expanded = expanded_source_popover.is_some_and(|expanded_id| expanded_id == id);
widget::settings::item(description, popover_button(id, expanded)).into()
}
fn popover_button(id: usize, expanded: bool) -> Element<'static, Message> {
let on_press = Message::ExpandLanguagePopover(if expanded { None } else { Some(id) });
let button = button::icon(widget::icon::from_name("view-more-symbolic"))
.extra_small()
.on_press(on_press);
if expanded {
widget::popover(button)
.popup(popover_menu(id))
.on_close(Message::ExpandLanguagePopover(None))
.into()
} else {
button.into()
}
}
fn popover_menu(id: usize) -> Element<'static, Message> {
widget::column::with_children(vec![
popover_menu_row(
id,
fl!("keyboard-sources", "move-up"),
SourceContext::MoveUp,
),
cosmic::widget::divider::horizontal::default().into(),
popover_menu_row(
id,
fl!("keyboard-sources", "move-down"),
SourceContext::MoveDown,
),
cosmic::widget::divider::horizontal::default().into(),
popover_menu_row(id, fl!("keyboard-sources", "remove"), SourceContext::Remove),
])
.padding(8)
.width(Length::Shrink)
.height(Length::Shrink)
.apply(cosmic::widget::container)
.class(cosmic::theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
container::Style {
icon_color: Some(theme.cosmic().background.on.into()),
text_color: Some(theme.cosmic().background.on.into()),
background: Some(Color::from(theme.cosmic().background.base).into()),
border: Border {
radius: cosmic.corner_radii.radius_m.into(),
..Default::default()
},
shadow: Default::default(),
}
}))
.into()
}
fn popover_menu_row(
id: usize,
label: String,
message: impl Fn(usize) -> SourceContext + 'static,
) -> cosmic::Element<'static, Message> {
widget::text::body(label)
.apply(widget::container)
.class(cosmic::theme::Container::custom(|theme| {
widget::container::Style {
background: None,
..widget::container::Catalog::style(theme, &cosmic::theme::Container::List)
}
}))
.apply(button::custom)
.on_press(())
.class(theme::Button::Transparent)
.apply(Element::from)
.map(move |()| Message::SourceContext(message(id)))
}
pub async fn set_locale(lang: String, region: String) {
eprintln!("setting locale lang={lang}, region={region}");
_ = tokio::process::Command::new("localectl")
.arg("set-locale")
.args(&[
["LANG=", &lang].concat(),
["LC_ADDRESS=", &region].concat(),
["LC_IDENTIFICATION=", &region].concat(),
["LC_MEASUREMENT=", &region].concat(),
["LC_MONETARY=", &region].concat(),
["LC_NAME=", &region].concat(),
["LC_NUMERIC=", &region].concat(),
["LC_PAPER=", &region].concat(),
["LC_TELEPHONE=", &region].concat(),
["LC_TIME=", &region].concat(),
])
.status()
.await;
}

View file

@ -23,24 +23,26 @@ pub fn color_picker_context_view<'a, Message: Clone + 'static>(
let theme = theme::active();
let spacing = &theme.cosmic().spacing;
cosmic::widget::column()
.push_maybe(description.map(|description| text(description).width(Length::Fill)))
.push(
model
.builder(on_update)
.reset_label(reset)
.height(Length::Fixed(158.0))
.build(
fl!("recent-colors"),
fl!("copy-to-clipboard"),
fl!("copied-to-clipboard"),
)
.apply(container)
.width(Length::Fixed(248.0))
.align_x(Alignment::Center)
.apply(container)
.center_x(Length::Fill),
let description = description.map(|description| text(description).width(Length::Fill));
let color_picker = model
.builder(on_update)
.reset_label(reset)
.height(Length::Fixed(158.0))
.build(
fl!("recent-colors"),
fl!("copy-to-clipboard"),
fl!("copied-to-clipboard"),
)
.apply(container)
.width(Length::Fixed(248.0))
.align_x(Alignment::Center)
.apply(container)
.center_x(Length::Fill);
cosmic::widget::column()
.push_maybe(description)
.push(color_picker)
.padding(spacing.space_l)
.align_x(Alignment::Center)
.spacing(spacing.space_m)