feat(keyboard): add layouts and their variants to the xkb config
This commit is contained in:
parent
2b88275af8
commit
42989b68a7
6 changed files with 308 additions and 69 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(§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<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
9
debian/control
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue