From 42989b68a7693bee2d9f9247d219318f81678a3e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 29 Mar 2024 14:18:18 +0100 Subject: [PATCH] feat(keyboard): add layouts and their variants to the xkb config --- Cargo.lock | 23 ++ Cargo.toml | 2 +- cosmic-settings/Cargo.toml | 1 + .../src/pages/input/keyboard/mod.rs | 339 ++++++++++++++---- debian/control | 9 +- i18n/en/cosmic_settings.ftl | 3 + 6 files changed, 308 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3557156..dd538f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,6 +1330,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "xkb-data", ] [[package]] @@ -4894,6 +4895,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.197" @@ -6585,6 +6598,16 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "xkb-data" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294a599fc9e6a43c9f44f5d6c560b89fd751be413717442b31c17fa367d3c764" +dependencies = [ + "serde", + "serde-xml-rs", +] + [[package]] name = "xkbcommon" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index d640454..449ba33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cosmic-settings", "page", "pages/*"] +members = [ "cosmic-settings", "page", "pages/*"] default-members = ["cosmic-settings"] resolver = "2" rust-version = "1.71.0" diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index 82569b7..41bbde6 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -46,6 +46,7 @@ static_init = "1.0.3" clap = { version = "4.4.18", features = ["derive"] } itoa = "1.0.10" futures = { package = "futures-lite", version = "2.2.0" } +xkb-data = "0.1.0" [dependencies.i18n-embed] version = "0.14.1" diff --git a/cosmic-settings/src/pages/input/keyboard/mod.rs b/cosmic-settings/src/pages/input/keyboard/mod.rs index c061b58..22c2023 100644 --- a/cosmic-settings/src/pages/input/keyboard/mod.rs +++ b/cosmic-settings/src/pages/input/keyboard/mod.rs @@ -1,19 +1,15 @@ use cosmic::{ cosmic_config::{self, ConfigSet}, - iced::{ - self, - widget::{self, horizontal_space}, - Length, - }, + iced::{self, Length}, iced_core::Border, iced_style, theme, - widget::{button, container, icon, radio, settings}, + widget::{self, button, container, icon, radio, settings}, Apply, Command, Element, }; use cosmic_comp_config::XkbConfig; use cosmic_settings_page::{self as page, section, Section}; use itertools::Itertools; -use slotmap::SlotMap; +use slotmap::{DefaultKey, SlotMap}; static COMPOSE_OPTIONS: &[(&str, &str)] = &[ // ("Left Alt", "compose:lalt"), XXX? @@ -41,16 +37,33 @@ static ALTERNATE_CHARACTER_OPTIONS: &[(&str, &str)] = &[ #[derive(Clone, Debug)] pub enum Message { - ExpandInputSourcePopover(Option), + ExpandInputSourcePopover(Option), OpenSpecialCharacterContext(SpecialKey), + ShowInputSourcesContext, + SourceAdd(DefaultKey), + SourceContext(SourceContext), SpecialCharacterSelect(Option<&'static str>), } +#[derive(Clone, Debug)] +pub enum SourceContext { + MoveDown(DefaultKey), + MoveUp(DefaultKey), + Remove(DefaultKey), + Settings(DefaultKey), + ViewLayout(DefaultKey), +} + +pub type Locale = String; +pub type Variant = String; +pub type Description = String; + pub struct Page { config: cosmic_config::Config, context: Option, - expanded_source_popover: Option, - sources: Vec, + expanded_source_popover: Option, + keyboard_layouts: SlotMap, + active_layouts: Vec, xkb: XkbConfig, } @@ -61,14 +74,16 @@ impl Default for Page { Self { context: None, expanded_source_popover: None, - sources: default_input_sources(), - xkb: super::get_config(&config, "xkb_config"), + keyboard_layouts: SlotMap::new(), + active_layouts: Vec::new(), + xkb: XkbConfig::default(), config, } } } enum Context { + ShowInputSourcesContext, SpecialCharacter(SpecialKey), } @@ -94,7 +109,11 @@ impl SpecialKey { } } -fn popover_menu_row(label: String) -> cosmic::Element<'static, Message> { +fn popover_menu_row( + id: DefaultKey, + label: String, + message: impl Fn(DefaultKey) -> SourceContext + 'static, +) -> cosmic::Element<'static, Message> { widget::text(label) .apply(widget::container) .style(cosmic::theme::Container::custom(|theme| { @@ -104,22 +123,41 @@ fn popover_menu_row(label: String) -> cosmic::Element<'static, Message> { } })) .apply(button) + .on_press(()) .style(theme::Button::Transparent) - .into() + .apply(Element::from) + .map(move |()| Message::SourceContext(message(id))) } -// TODO for on press, would need to clone ID for each row? -fn popover_menu() -> cosmic::Element<'static, Message> { - // XXX translate - widget::column![ - popover_menu_row(fl!("keyboard-sources", "move-up")), - popover_menu_row(fl!("keyboard-sources", "move-down")), - //cosmic::widget::divider::horizontal::light(), - cosmic::widget::divider::horizontal::light(), - popover_menu_row(fl!("keyboard-sources", "settings")), - popover_menu_row(fl!("keyboard-sources", "view-layout")), - popover_menu_row(fl!("keyboard-sources", "remove")), - ] +fn popover_menu(id: DefaultKey) -> cosmic::Element<'static, Message> { + widget::column::with_children(vec![ + popover_menu_row( + id, + fl!("keyboard-sources", "move-up"), + SourceContext::MoveUp, + ) + .into(), + popover_menu_row( + id, + fl!("keyboard-sources", "move-down"), + SourceContext::MoveDown, + ) + .into(), + cosmic::widget::divider::horizontal::light().into(), + popover_menu_row( + id, + fl!("keyboard-sources", "settings"), + SourceContext::Settings, + ) + .into(), + popover_menu_row( + id, + fl!("keyboard-sources", "view-layout"), + SourceContext::ViewLayout, + ) + .into(), + popover_menu_row(id, fl!("keyboard-sources", "remove"), SourceContext::Remove).into(), + ]) .width(Length::Shrink) .height(Length::Shrink) .apply(cosmic::widget::container) @@ -139,41 +177,34 @@ fn popover_menu() -> cosmic::Element<'static, Message> { .into() } -fn popover_button(input_source: &InputSource, expanded: bool) -> cosmic::Element<'static, Message> { - let on_press = Message::ExpandInputSourcePopover(if expanded { - None - } else { - Some(input_source.id.clone()) - }); +fn popover_button(id: DefaultKey, expanded: bool) -> cosmic::Element<'static, Message> { + let on_press = Message::ExpandInputSourcePopover(if expanded { None } else { Some(id) }); let button = button::icon(icon::from_name("open-menu-symbolic")) .extra_small() - .padding(0) .on_press(on_press); if expanded { - cosmic::widget::popover(button).popup(popover_menu()).into() + cosmic::widget::popover(button) + .popup(popover_menu(id)) + .into() } else { button.into() } } -fn input_source<'a>( - input_source: &'a InputSource, - expanded_source_popover: Option<&'a str>, -) -> cosmic::Element<'a, Message> { - let expanded = expanded_source_popover == Some(input_source.id.as_str()); - settings::item(&input_source.label, popover_button(input_source, expanded)).into() +fn input_source( + id: DefaultKey, + description: &str, + expanded_source_popover: Option, +) -> cosmic::Element { + let expanded = expanded_source_popover.is_some_and(|expanded_id| expanded_id == id); + + settings::item(description, popover_button(id, expanded)).into() } pub mod shortcuts; -pub struct InputSource { - id: String, - // TODO Translate? - label: String, -} - fn special_char_radio_row<'a>( desc: &'a str, value: Option<&'static str>, @@ -186,14 +217,6 @@ fn special_char_radio_row<'a>( .into() } -// XXX -pub fn default_input_sources() -> Vec { - vec![InputSource { - id: "us".to_string(), - label: "English (US)".to_string(), - }] -} - impl page::Page for Page { fn content( &self, @@ -214,6 +237,7 @@ impl page::Page for Page { fn context_drawer(&self) -> Option> { match self.context { + Some(Context::ShowInputSourcesContext) => Some(self.add_input_source_view()), Some(Context::SpecialCharacter(special_key)) => self .special_character_key_view(special_key) .map(crate::pages::Message::Keyboard) @@ -222,11 +246,124 @@ impl page::Page for Page { None => None, } } + + fn reload(&mut self, _page: page::Entity) -> Command { + self.xkb = super::get_config(&self.config, "xkb_config"); + match xkb_data::keyboard_layouts() { + Ok(keyboard_layouts) => { + self.active_layouts.clear(); + self.keyboard_layouts.clear(); + + for layout in keyboard_layouts.layouts() { + self.keyboard_layouts.insert(( + layout.name().to_owned(), + String::new(), + layout.description().to_owned(), + )); + + if let Some(variants) = layout.variants() { + for variant in variants { + self.keyboard_layouts.insert(( + layout.name().to_owned(), + variant.name().to_owned(), + variant.description().to_owned(), + )); + } + } + } + + // Xkb layouts currently enabled. + let layouts = self.xkb.layout.split_terminator(','); + + // Xkb variants for each layout. Repeat empty strings in case there's more layouts than variants. + let variants = self + .xkb + .variant + .split_terminator(',') + .chain(std::iter::repeat("")); + + for (layout, variant) in layouts.zip(variants) { + for (id, (xkb_layout, xkb_variant, _desc)) in &self.keyboard_layouts { + if layout == xkb_layout && variant == xkb_variant { + self.active_layouts.push(id); + } + } + } + } + + Err(why) => { + tracing::error!(?why, "failed to get keyboard layouts"); + } + } + + Command::none() + } } impl Page { pub fn update(&mut self, message: Message) -> Command { match message { + Message::SourceAdd(id) => { + self.context = None; + + if !self.active_layouts.contains(&id) { + self.active_layouts.push(id); + self.update_xkb_config(); + } + } + + Message::SourceContext(context_message) => { + self.expanded_source_popover = None; + + match context_message { + SourceContext::MoveDown(id) => { + if let Some(pos) = + self.active_layouts.iter().position(|&active| active == id) + { + if pos + 1 < self.active_layouts.len() { + self.active_layouts.swap(pos, pos + 1); + self.update_xkb_config(); + } + } + } + + SourceContext::MoveUp(id) => { + if let Some(pos) = + self.active_layouts.iter().position(|&active| active == id) + { + if pos > 0 { + self.active_layouts.swap(pos, pos - 1); + self.update_xkb_config(); + } + } + } + + SourceContext::Remove(id) => { + if let Some(pos) = + self.active_layouts.iter().position(|&active| active == id) + { + let _removed = self.active_layouts.remove(pos); + self.update_xkb_config(); + } + } + + SourceContext::Settings(id) => { + eprintln!("settings not implemented"); + } + + SourceContext::ViewLayout(id) => { + eprintln!("view layout not implemented"); + } + } + } + + Message::ShowInputSourcesContext => { + self.context = Some(Context::ShowInputSourcesContext); + return cosmic::command::message(crate::app::Message::OpenContextDrawer( + fl!("keyboard-sources", "add").into(), + )); + } + Message::ExpandInputSourcePopover(value) => { self.expanded_source_popover = value; } @@ -240,7 +377,7 @@ impl Page { Message::SpecialCharacterSelect(id) => { if let Some(Context::SpecialCharacter(special_key)) = self.context { - let options = self.xkb.options.as_deref().unwrap_or(""); + let options = self.xkb.options.as_deref().unwrap_or_default(); let prefix = special_key.prefix(); let new_options = options .split(',') @@ -260,8 +397,41 @@ impl Page { Command::none() } - pub fn add_input_source_view(&self) -> cosmic::Element<'static, crate::app::Message> { - widget::column![].into() + pub fn add_input_source_view(&self) -> Element<'_, crate::pages::Message> { + let mut list = widget::list_column(); + + for (id, (_locale, variant, description)) in &self.keyboard_layouts { + list = list.add(self.input_source_item(id, description, !variant.is_empty())); + } + + widget::column() + .push(list) + .apply(Element::from) + .map(crate::pages::Message::Keyboard) + } + + fn input_source_item<'a>( + &self, + id: DefaultKey, + description: &'a str, + indent: bool, + ) -> Element<'a, Message> { + let is_added = self.active_layouts.contains(&id); + let button_text = if is_added { fl!("added") } else { fl!("add") }; + + let add_button = widget::button::text(button_text).on_press_maybe(if is_added { + None + } else { + Some(Message::SourceAdd(id)) + }); + + let button = widget::settings::item::builder(description).control(add_button); + + if indent { + container(button).padding([0, 0, 0, 16]).into() + } else { + button.into() + } } fn special_character_key_view(&self, special_key: SpecialKey) -> cosmic::Element<'_, Message> { @@ -290,6 +460,30 @@ impl Page { .height(Length::Fill) .into() } + + fn update_xkb_config(&mut self) { + let mut new_layout = String::new(); + let mut new_variant = String::new(); + + for id in &self.active_layouts { + if let Some((locale, variant, _description)) = self.keyboard_layouts.get(*id) { + new_layout.push_str(locale); + new_layout.push(','); + new_variant.push_str(variant); + new_variant.push(','); + } + } + + let _excess_comma = new_layout.pop(); + let _excess_comma = new_variant.pop(); + + self.xkb.layout = new_layout; + self.xkb.variant = new_variant; + + if let Err(err) = self.config.set("xkb_config", &self.xkb) { + tracing::error!(?err, "Failed to set config 'xkb_config'"); + } + } } impl page::AutoBind for Page { @@ -299,20 +493,31 @@ impl page::AutoBind for Page { } fn input_sources() -> Section { - // TODO desc Section::default() .title(fl!("keyboard-sources")) .view::(|_binder, page, section| { // TODO Need something more custom, with drag and drop let mut section = settings::view_section(§ion.title); - let expanded_source = page.expanded_source_popover.as_deref(); - for source in &page.sources { - section = section.add(input_source(source, expanded_source)); + for id in &page.active_layouts { + if let Some((_locale, _variant, description)) = page.keyboard_layouts.get(*id) { + section = + section.add(input_source(*id, description, page.expanded_source_popover)); + } } - section - .apply(cosmic::Element::from) + let add_input_source = widget::button::standard(fl!("keyboard-sources", "add")) + .on_press(Message::ShowInputSourcesContext); + + widget::column::with_capacity(2) + .spacing(cosmic::theme::active().cosmic().space_xxs()) + .push(section) + .push( + widget::container(add_input_source) + .width(Length::Fill) + .align_x(iced::alignment::Horizontal::Right), + ) + .apply(Element::from) .map(crate::pages::Message::Keyboard) }) } @@ -364,10 +569,10 @@ fn keyboard_shortcuts() -> Section { } fn go_next_control() -> cosmic::Element<'static, Msg> { - widget::row!( - horizontal_space(Length::Fill), - icon::from_name("go-next-symbolic").size(16).icon(), - ) + widget::row::with_children(vec![ + widget::horizontal_space(Length::Fill).into(), + icon::from_name("go-next-symbolic").size(16).icon().into(), + ]) .into() } diff --git a/debian/control b/debian/control index ec1e09e..5909bd5 100644 --- a/debian/control +++ b/debian/control @@ -21,6 +21,13 @@ Homepage: https://github.com/pop-os/cosmic-settings Package: cosmic-settings Architecture: amd64 arm64 -Depends: ${misc:Depends}, ${shlibs:Depends}, cosmic-randr +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + accountsservice, + cosmic-randr, + gettext, + iso-codes, + xkb-data, Description: Settings application for the COSMIC desktop environment Settings application for the COSMIC desktop environment diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index d5f3941..1d5fbec 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -399,11 +399,14 @@ keyboard-sources = Input Sources .settings = Settings .view-layout = View keyboard layout .remove = Remove + .add = Add input source keyboard-special-char = Special Character Entry .alternate = Alternate characters key .compose = Compose key +added = Added + ## Input: Keyboard: Shortcuts keyboard-shortcuts = Keyboard Shortcuts