feat(keyboard): add layouts and their variants to the xkb config

This commit is contained in:
Michael Aaron Murphy 2024-03-29 14:18:18 +01:00 committed by Michael Murphy
parent 2b88275af8
commit 42989b68a7
6 changed files with 308 additions and 69 deletions

23
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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<String>),
ExpandInputSourcePopover(Option<DefaultKey>),
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<Context>,
expanded_source_popover: Option<String>,
sources: Vec<InputSource>,
expanded_source_popover: Option<DefaultKey>,
keyboard_layouts: SlotMap<DefaultKey, (Locale, Variant, Description)>,
active_layouts: Vec<DefaultKey>,
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<DefaultKey>,
) -> cosmic::Element<Message> {
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<InputSource> {
vec![InputSource {
id: "us".to_string(),
label: "English (US)".to_string(),
}]
}
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
@ -214,6 +237,7 @@ impl page::Page<crate::pages::Message> for Page {
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
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<crate::pages::Message> for Page {
None => None,
}
}
fn reload(&mut self, _page: page::Entity) -> Command<crate::pages::Message> {
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<crate::app::Message> {
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<crate::pages::Message> for Page {
@ -299,20 +493,31 @@ impl page::AutoBind<crate::pages::Message> for Page {
}
fn input_sources() -> Section<crate::pages::Message> {
// TODO desc
Section::default()
.title(fl!("keyboard-sources"))
.view::<Page>(|_binder, page, section| {
// TODO Need something more custom, with drag and drop
let mut section = settings::view_section(&section.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<crate::pages::Message> {
}
fn go_next_control<Msg: Clone + 'static>() -> 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()
}

9
debian/control vendored
View file

@ -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

View file

@ -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