Merge pull request #61 from pop-os/input-settings

Add input settings, with mouse, keyboard, keyboard shortcuts sub-pages
This commit is contained in:
Ian Douglas Scott 2023-09-07 07:54:53 -07:00 committed by GitHub
commit c8148c8f48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1078 additions and 32 deletions

122
Cargo.lock generated
View file

@ -253,7 +253,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -270,7 +270,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -445,7 +445,7 @@ checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -732,6 +732,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "cosmic-comp-config"
version = "0.1.0"
source = "git+https://github.com/pop-os/cosmic-comp#1392fc7c953678a14825ba3d1e5619d38c1946c7"
dependencies = [
"input",
"serde",
]
[[package]]
name = "cosmic-config"
version = "0.1.0"
@ -778,11 +787,13 @@ dependencies = [
"apply",
"async-channel",
"color-eyre",
"cosmic-comp-config",
"cosmic-panel-config",
"cosmic-settings-desktop",
"cosmic-settings-page",
"cosmic-settings-system",
"cosmic-settings-time",
"derivative",
"derive_setters",
"dirs 5.0.1",
"downcast-rs",
@ -792,12 +803,14 @@ dependencies = [
"i18n-embed",
"i18n-embed-fl",
"image",
"itertools 0.11.0",
"libcosmic",
"log",
"notify",
"once_cell",
"regex",
"rust-embed",
"serde",
"slotmap",
"tokio",
"tracing",
@ -1069,7 +1082,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -1091,7 +1104,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [
"darling_core 0.20.1",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -1133,7 +1146,7 @@ dependencies = [
"darling 0.20.1",
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -1219,7 +1232,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -1292,7 +1305,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -1576,7 +1589,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -1739,7 +1752,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -2083,7 +2096,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.22",
"syn 2.0.28",
"unic-langid",
]
@ -2206,7 +2219,7 @@ dependencies = [
"iced_graphics",
"iced_runtime",
"iced_style",
"itertools",
"itertools 0.10.5",
"raw-window-handle 0.5.2",
"smithay-client-toolkit 0.17.0",
"smithay-clipboard",
@ -2441,6 +2454,29 @@ dependencies = [
"libc",
]
[[package]]
name = "input"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e74cd82cedcd66db78742a8337bdc48f188c4d2c12742cbc5cd85113f0b059"
dependencies = [
"bitflags 1.3.2",
"input-sys",
"io-lifetimes",
"libc",
"log",
"udev",
]
[[package]]
name = "input-sys"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f6c2a17e8aba7217660e32863af87b0febad811d4b8620ef76b386603fddc2"
dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -2504,6 +2540,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
@ -2649,6 +2694,16 @@ version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
[[package]]
name = "libudev-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
@ -3260,7 +3315,7 @@ checksum = "3c02bfa6b3ba8af5434fa0531bf5701f750d983d4260acd6867faca51cdc4484"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -3353,7 +3408,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -3388,7 +3443,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -3737,7 +3792,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.22",
"syn 2.0.28",
"walkdir",
]
@ -3860,22 +3915,22 @@ checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af"
[[package]]
name = "serde"
version = "1.0.164"
version = "1.0.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.164"
version = "1.0.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -3886,7 +3941,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -4205,9 +4260,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.22"
version = "2.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616"
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
dependencies = [
"proc-macro2",
"quote",
@ -4297,7 +4352,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -4487,7 +4542,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
]
[[package]]
@ -4567,6 +4622,17 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "udev"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebdbbd670373442a12fe9ef7aeb53aec4147a5a27a00bbc3ab639f08f48191a"
dependencies = [
"libc",
"libudev-sys",
"pkg-config",
]
[[package]]
name = "uds_windows"
version = "1.0.2"
@ -4813,7 +4879,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
"wasm-bindgen-shared",
]
@ -4847,7 +4913,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.22",
"syn 2.0.28",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View file

@ -16,6 +16,10 @@ git = "https://github.com/pop-os/libcosmic"
[workspace.dependencies.cosmic-bg-config]
git = "https://github.com/pop-os/cosmic-bg"
[workspace.dependencies.cosmic-comp-config]
git = "https://github.com/pop-os/cosmic-comp"
# path = "../cosmic-comp/cosmic-comp-config"
[workspace.dependencies.cosmic-panel-config]
git = "https://github.com/pop-os/cosmic-panel"

View file

@ -13,10 +13,12 @@ cosmic-settings-desktop = { path = "../pages/desktop" }
cosmic-settings-page = { path = "../page" }
cosmic-settings-system = { path = "../pages/system" }
cosmic-settings-time = { path = "../pages/time" }
derivative = "2.2.0"
derive_setters = "0.1.6"
dirs = "5.0.1"
generator = "0.7.4"
i18n-embed-fl = "0.6.7"
itertools = "0.11.0"
libcosmic = {workspace = true}
once_cell = "1.17.2"
regex = "1.8.3"
@ -24,6 +26,7 @@ rust-embed = "6.6.1"
slotmap = "1.0.6"
tokio = "1.28.2"
downcast-rs = "1.2.0"
cosmic-comp-config = { workspace = true }
# TODO: migrate this dependency to the pages/desktop crate.
cosmic-panel-config = { workspace = true }
tracing = "0.1.37"
@ -35,6 +38,7 @@ freedesktop-desktop-entry = "0.5.0"
notify = "6.0.0"
anyhow = "1.0"
image = "0.24.6"
serde = { version = "1.0.180", features = ["derive"] }
[dependencies.i18n-embed]
version = "0.13.9"

View file

@ -39,6 +39,7 @@ use crate::{
applets::{self, APPLET_DND_ICON_ID},
},
},
input::{self, keyboard},
sound, system, time,
},
subscription::desktop_files,
@ -142,6 +143,8 @@ impl Application for SettingsApp {
// app.insert_page::<accessibility::Page>();
// app.insert_page::<applications::Page>();
//
app.insert_page::<input::Page>();
let active_id = app
.pages
@ -268,6 +271,11 @@ impl Application for SettingsApp {
crate::pages::Message::DesktopWallpaper(message) => {
page::update!(self.pages, message, desktop::wallpaper::Page);
}
crate::pages::Message::Input(message) => {
if let Some(page) = self.pages.page_mut::<input::Page>() {
return page.update(message);
}
}
crate::pages::Message::External { .. } => {
todo!("external plugins not supported yet");
}
@ -326,6 +334,16 @@ impl Application for SettingsApp {
{
return page.add_applet_view();
}
if let Some(Some(page)) =
(id == keyboard::ADD_INPUT_SOURCE_DIALOGUE_ID).then(|| self.pages.page::<input::Page>())
{
return page.add_input_source_view();
}
if let Some(Some(page)) = (id == keyboard::SPECIAL_CHARACTER_DIALOGUE_ID)
.then(|| self.pages.page::<input::Page>())
{
return page.special_character_key_view();
}
cosmic::iced::widget::responsive(|size| {
let is_condensed = (600.0 * self.scaling_factor) > size.width;

View file

@ -0,0 +1,331 @@
use apply::Apply;
use cosmic::{
iced::{
self,
widget::{self, horizontal_space},
window, Length,
},
iced_style, theme,
widget::settings,
};
use cosmic_settings_page::{self as page, section, Section};
use slotmap::SlotMap;
use super::Message;
pub const ADD_INPUT_SOURCE_DIALOGUE_ID: window::Id = window::Id(2000);
pub const SPECIAL_CHARACTER_DIALOGUE_ID: window::Id = window::Id(2001);
static COMPOSE_OPTIONS: &[(&str, &str)] = &[
// ("Left Alt", "compose:lalt"), XXX?
("Right Alt", "compose:ralt"),
("Left Super", "compose:lwin"),
("Right Super", "compose:rwin"),
("Menu key", "compose:menu"),
("Right Ctrl", "compose:rctrl"),
("Caps Lock", "compose:caps"),
("Scroll Lock", "compose:sclk"),
("Print Screen", "compose:prsc"),
];
static ALTERNATE_CHARACTER_OPTIONS: &[(&str, &str)] = &[
("Left Alt", "lv3:lalt_switch"),
("Right Alt", "lv3:alt_switch"),
("Left Super", "lv3:lwin_switch"),
("Right Super", "lv3:win_switch"),
("Menu key", "lv3:menu_switch"),
// ("Right Ctrl", "lv3:"), XXX
("Caps Lock", "lv3:caps_switch"),
// ("Scroll Lock", "lv3:"), XXX
// ("Print Screen", "lv3"), XXX
];
#[derive(Copy, Clone, Debug)]
pub enum SpecialKey {
AlternateCharacters,
Compose,
}
impl SpecialKey {
pub fn title(self) -> String {
match self {
Self::Compose => "Compose".to_string(),
Self::AlternateCharacters => "Alternate Characters".to_string(),
}
}
pub fn prefix(self) -> &'static str {
match self {
Self::Compose => "compose:",
Self::AlternateCharacters => "lv3:",
}
}
}
fn popover_menu_row(label: String) -> cosmic::Element<'static, Message> {
widget::text(label)
.apply(widget::container)
.style(cosmic::theme::Container::custom(|theme| {
iced_style::container::Appearance {
background: None,
..cosmic::widget::list::column::style(theme)
}
}))
.apply(widget::button)
.style(cosmic::theme::Button::Transparent)
.into()
}
// 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")),
]
.width(Length::Shrink)
.height(Length::Shrink)
.apply(cosmic::widget::container)
.style(cosmic::theme::Container::custom(|theme| {
iced_style::container::Appearance {
text_color: Some(theme.cosmic().background.on.into()),
background: Some(iced::Color::from(theme.cosmic().background.base).into()),
border_radius: (12.0).into(),
border_width: 0.0,
border_color: iced::Color::TRANSPARENT,
}
}))
.into()
}
fn popover_button(input_source: &InputSource, expanded: bool) -> cosmic::Element<'static, Message> {
let style = if expanded {
cosmic::theme::Svg::SymbolicActive
} else {
cosmic::theme::Svg::Symbolic
};
let on_press = Message::ExpandInputSourcePopover(if expanded {
None
} else {
Some(input_source.id.clone())
});
let button = cosmic::widget::button(cosmic::theme::Button::Secondary)
.icon(style, "open-menu-symbolic", 20)
.padding(0)
.on_press(on_press);
if expanded {
cosmic::widget::popover(button, popover_menu()).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()
}
pub mod shortcuts;
pub struct InputSource {
id: String,
// TODO Translate?
label: String,
}
impl super::Page {
pub fn add_input_source_view(&self) -> cosmic::Element<'static, crate::app::Message> {
widget::column![].into()
}
pub fn special_character_key_view(&self) -> cosmic::Element<'_, crate::app::Message> {
let Some(special_key) = self.special_character_dialog else {
return widget::text("").into();
};
let options = match special_key {
SpecialKey::Compose => COMPOSE_OPTIONS,
SpecialKey::AlternateCharacters => ALTERNATE_CHARACTER_OPTIONS,
};
let prefix = special_key.prefix();
let current = self
.xkb
.options
.iter()
.flat_map(|x| x.split(','))
.find(|x| x.starts_with(prefix));
// TODO description, layout default
let mut list = cosmic::widget::list_column();
list = list.add(special_char_radio_row("None", None, current));
for (desc, id) in options {
list = list.add(special_char_radio_row(desc, Some(id), current));
}
widget::column![
cosmic::widget::header_bar()
.title(special_key.title())
.on_close(Message::CloseSpecialCharacterDialog),
cosmic::widget::container(
cosmic::widget::scrollable(cosmic::widget::container(list).padding(24))
.width(Length::Fill)
.height(Length::Fill)
)
.style(theme::Container::Background)
.width(Length::Fill)
.height(Length::Fill)
]
.apply(cosmic::Element::from)
.map(crate::pages::Message::Input)
.map(crate::app::Message::PageMessage)
}
}
fn special_char_radio_row<'a>(
desc: &'a str,
value: Option<&'static str>,
current_value: Option<&'a str>,
) -> cosmic::Element<'a, Message> {
settings::item_row(vec![iced::widget::radio(
desc,
value,
Some(current_value),
|_| Message::SpecialCharacterSelect(value),
)
.into()])
.into()
}
#[derive(Default)]
pub struct Page;
// 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,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![
sections.insert(input_sources()),
sections.insert(special_character_entry()),
sections.insert(keyboard_shortcuts()),
])
}
fn info(&self) -> page::Info {
page::Info::new("keyboard", "input-keyboard-symbolic")
.title(fl!("keyboard"))
.description(fl!("keyboard", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
page.sub_page::<shortcuts::Page>()
}
}
fn input_sources() -> Section<crate::pages::Message> {
// TODO desc
Section::default()
.title(fl!("keyboard-sources"))
.view::<Page>(|binder, _page, section| {
let input = binder.page::<super::Page>().expect("input page not found");
// TODO Need something more custom, with drag and drop
let mut section = settings::view_section(&section.title);
let expanded_source = input.expanded_source_popover.as_deref();
for source in &input.sources {
section = section.add(input_source(source, expanded_source));
}
section
.apply(cosmic::Element::from)
.map(crate::pages::Message::Input)
})
}
fn special_character_entry() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("keyboard-special-char"))
.descriptions(vec![
fl!("keyboard-special-char", "alternate"),
fl!("keyboard-special-char", "compose"),
])
.view::<Page>(|_binder, _page, section| {
let descriptions = &section.descriptions;
// TODO dialogs
settings::view_section(&section.title)
.add(go_next_item(
&descriptions[0],
Message::OpenSpecialCharacterDialog(SpecialKey::AlternateCharacters),
))
.add(go_next_item(
&descriptions[1],
Message::OpenSpecialCharacterDialog(SpecialKey::Compose),
))
.apply(cosmic::Element::from)
.map(crate::pages::Message::Input)
})
}
fn keyboard_shortcuts() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("keyboard-shortcuts"))
.descriptions(vec![fl!("keyboard-shortcuts", "desc")])
.view::<Page>(|binder, _page, section| {
let descriptions = &section.descriptions;
let mut section = settings::view_section(&section.title);
if let Some((shortcuts_entity, _)) = binder
.info
.iter()
.find(|(_, v)| v.id == "keyboard-shortcuts")
{
section = section.add(go_next_item(
&descriptions[0],
crate::pages::Message::Page(shortcuts_entity),
));
}
section.apply(cosmic::Element::from)
})
}
fn go_next_control<Msg: Clone + 'static>() -> cosmic::Element<'static, Msg> {
widget::row!(
horizontal_space(Length::Fill),
cosmic::widget::icon("go-next-symbolic", 20).style(cosmic::theme::Svg::Symbolic)
)
.into()
}
fn go_next_item<Msg: Clone + 'static>(description: &str, msg: Msg) -> cosmic::Element<'_, Msg> {
settings::item(description, go_next_control())
.apply(widget::container)
.style(cosmic::theme::Container::custom(
cosmic::widget::list::column::style,
))
.apply(widget::button)
.style(cosmic::theme::Button::Transparent)
.padding(0)
.on_press(msg)
.into()
}

View file

@ -0,0 +1,51 @@
use apply::Apply;
use cosmic::iced::widget;
use cosmic::widget::settings;
use cosmic::Element;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
//crate::app::Message::Page
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn info(&self) -> page::Info {
page::Info::new("keyboard-shortcuts", "input-keyboard-symbolic")
.title(fl!("keyboard-shortcuts"))
.description(fl!("keyboard-shortcuts", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
fn shortcuts() -> Section<crate::pages::Message> {
Section::default()
.descriptions(vec![])
.view::<Page>(|binder, _page, section| {
let _descriptions = &section.descriptions;
let _input = binder
.page::<super::super::Page>()
.expect("input page not found");
// TODO need something more custom
/*
settings::view_section(&section.title)
.apply(Element::from)
.map(crate::pages::Message::Input)
*/
widget::column![settings::view_section(&section.title)]
.apply(Element::from)
.map(crate::pages::Message::Input)
})
}

221
app/src/pages/input/mod.rs Normal file
View file

@ -0,0 +1,221 @@
use crate::app;
use cosmic::{
cosmic_config::{self, ConfigGet, ConfigSet},
iced::{self, wayland::actions::window::SctkWindowSettings, window},
iced_sctk::commands,
iced_widget::core::layout,
};
use cosmic_comp_config::{
input::{AccelProfile, InputConfig},
XkbConfig,
};
use cosmic_settings_page as page;
use itertools::Itertools;
use tracing::error;
pub mod keyboard;
mod mouse;
mod touchpad;
#[derive(Clone, Debug)]
pub enum Message {
SetAcceleration(bool, bool),
SetNaturalScroll(bool, bool),
SetScrollFactor(f64, bool),
SetDoubleClickSpeed(u32, bool),
SetMouseSpeed(f64, bool),
PrimaryButtonSelected(cosmic::widget::segmented_button::Entity, bool),
// seperate close message, to make sure another isn't closed?
ExpandInputSourcePopover(Option<String>),
OpenSpecialCharacterDialog(keyboard::SpecialKey),
CloseSpecialCharacterDialog,
SpecialCharacterSelect(Option<&'static str>),
}
pub struct Page {
config: cosmic_config::Config,
input_default: InputConfig,
#[allow(dead_code)]
input_touchpad: InputConfig,
// Mouse
primary_button: cosmic::widget::segmented_button::SingleSelectModel,
// Touchpad
touchpad_primary_button: cosmic::widget::segmented_button::SingleSelectModel,
// Keyboard
expanded_source_popover: Option<String>,
sources: Vec<keyboard::InputSource>,
special_character_dialog: Option<keyboard::SpecialKey>,
xkb: XkbConfig,
}
fn get_config<T: Default + serde::de::DeserializeOwned>(
config: &cosmic_config::Config,
key: &str,
) -> T {
config.get(key).unwrap_or_else(|err| {
error!(?err, "Failed to read config '{}'", key);
T::default()
})
}
impl Default for Page {
fn default() -> Self {
let config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
let input_default: InputConfig = get_config(&config, "input-default");
let input_touchpad: InputConfig = get_config(&config, "input-touchpad");
let xkb = get_config(&config, "xkb-config");
let mut primary_button = mouse::default_primary_button();
let idx = if input_default.left_handed.unwrap_or(false) {
1
} else {
0
};
primary_button.activate_position(idx);
let mut touchpad_primary_button = mouse::default_primary_button();
let idx = if input_touchpad.left_handed.unwrap_or(false) {
1
} else {
0
};
touchpad_primary_button.activate_position(idx);
Self {
config,
input_default,
input_touchpad,
// Mouse
primary_button,
// Touchpad
touchpad_primary_button,
// Keyboard
expanded_source_popover: None,
sources: keyboard::default_input_sources(),
special_character_dialog: None,
xkb,
}
}
}
impl Page {
fn update_input<F: Fn(&mut InputConfig)>(&mut self, touchpad: bool, f: F) {
let (name, input_config) = if touchpad {
("input-touchpad", &mut self.input_touchpad)
} else {
("input-default", &mut self.input_default)
};
f(input_config);
if let Err(err) = self.config.set(name, input_config) {
error!(?err, "Failed to set config '{}'", name);
}
}
pub fn update(&mut self, message: Message) -> iced::Command<app::Message> {
match message {
Message::SetAcceleration(value, touchpad) => {
let profile = if value {
AccelProfile::Adaptive
} else {
AccelProfile::Flat
};
self.update_input(touchpad, |x| {
x.acceleration.get_or_insert(Default::default()).profile = Some(profile);
});
}
Message::SetNaturalScroll(value, touchpad) => self.update_input(touchpad, |x| {
x.scroll_config
.get_or_insert(Default::default())
.natural_scroll = Some(value);
}),
Message::SetScrollFactor(value, touchpad) => self.update_input(touchpad, |x| {
x.scroll_config
.get_or_insert(Default::default())
.scroll_factor = Some(value)
}),
Message::SetDoubleClickSpeed(_value, _touchpad) => {
// TODO
}
Message::SetMouseSpeed(value, touchpad) => self.update_input(touchpad, |x| {
x.acceleration.get_or_insert(Default::default()).speed = value
}),
Message::PrimaryButtonSelected(entity, touchpad) => {
let select_model = if touchpad {
&mut self.touchpad_primary_button
} else {
&mut self.primary_button
};
select_model.activate(entity);
let left_entity = select_model.entity_at(1).unwrap();
let left_handed = select_model.active() == left_entity;
self.update_input(touchpad, |x| x.left_handed = Some(left_handed));
}
Message::ExpandInputSourcePopover(value) => {
self.expanded_source_popover = value;
}
Message::OpenSpecialCharacterDialog(special_key) => {
self.special_character_dialog = Some(special_key);
let window_settings = SctkWindowSettings {
window_id: keyboard::SPECIAL_CHARACTER_DIALOGUE_ID,
app_id: Some("com.system76.CosmicSettings".to_string()),
title: Some(special_key.title()),
parent: Some(window::Id(0)),
autosize: false,
size_limits: layout::Limits::NONE
.min_width(300.0)
.max_width(800.0)
.min_height(200.0)
.max_height(1080.0),
size: (512, 420),
resizable: None,
client_decorations: true,
transparent: true,
};
return commands::window::get_window(window_settings);
}
Message::CloseSpecialCharacterDialog => {
self.special_character_dialog = None;
return commands::window::close_window(keyboard::SPECIAL_CHARACTER_DIALOGUE_ID);
}
Message::SpecialCharacterSelect(id) => {
if let Some(special_key) = self.special_character_dialog {
let options = self.xkb.options.as_deref().unwrap_or("");
let prefix = special_key.prefix();
let new_options = options
.split(',')
.filter(|x| !x.starts_with(prefix))
.chain(id.into_iter())
.join(",");
self.xkb.options = Some(new_options).filter(|x| !x.is_empty());
if let Err(err) = self.config.set("xkb-config", &self.xkb) {
error!(?err, "Failed to set config 'xkb-config'");
}
}
}
}
iced::Command::none()
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
// XXX icon?
page::Info::new("input", "input-keyboard-symbolic")
.title(fl!("input"))
.description(fl!("input", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
page.sub_page::<keyboard::Page>()
.sub_page::<mouse::Page>()
.sub_page::<touchpad::Page>()
}
}

View file

@ -0,0 +1,147 @@
use apply::Apply;
use cosmic::iced::widget;
use cosmic::widget::settings;
use cosmic::Element;
use cosmic_comp_config::input::AccelProfile;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
use super::Message;
pub fn default_primary_button() -> cosmic::widget::segmented_button::SingleSelectModel {
let mut model = cosmic::widget::segmented_button::SingleSelectModel::builder()
.insert(|b| b.text(fl!("mouse", "primary-button-left")))
.insert(|b| b.text(fl!("mouse", "primary-button-right")))
.build();
model.activate_position(0);
model
}
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(mouse()), sections.insert(scrolling())])
}
fn info(&self) -> page::Info {
page::Info::new("mouse", "input-mouse-symbolic")
.title(fl!("mouse"))
.description(fl!("mouse", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
fn mouse() -> Section<crate::pages::Message> {
Section::default()
.descriptions(vec![
fl!("mouse", "primary-button"),
fl!("mouse", "speed"),
fl!("mouse", "acceleration"),
fl!("mouse", "acceleration-desc"),
fl!("mouse", "double-click-speed"),
fl!("mouse", "double-click-speed-desc"),
])
.view::<Page>(|binder, _page, section| {
let descriptions = &section.descriptions;
let input = binder.page::<super::Page>().expect("input page not found");
settings::view_section(&section.title)
.add(settings::item(
&descriptions[0],
cosmic::widget::segmented_selection::horizontal(&input.primary_button)
.on_activate(|x| Message::PrimaryButtonSelected(x, false)),
))
.add(
settings::item::builder(&descriptions[1]).control(widget::slider(
0.0..=100.0,
(input
.input_default
.acceleration
.as_ref()
.map_or(0.0, |x| x.speed)
+ 1.0)
* 50.0,
|value| Message::SetMouseSpeed((value / 50.0) - 1.0, false),
)),
)
.add(
settings::item::builder(&descriptions[2])
.description(&descriptions[3])
.toggler(
input
.input_default
.acceleration
.as_ref()
.map_or(true, |x| x.profile == Some(AccelProfile::Adaptive)),
|x| Message::SetAcceleration(x, false),
),
)
.add(
settings::item::builder(&descriptions[4])
.description(&descriptions[5])
.control(widget::slider(0..=100, 0, |x| {
Message::SetDoubleClickSpeed(x, false)
})),
)
.apply(Element::from)
.map(crate::pages::Message::Input)
})
}
fn scrolling() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("mouse-scrolling"))
.descriptions(vec![
fl!("mouse-scrolling", "speed"),
fl!("mouse-scrolling", "natural"),
fl!("mouse-scrolling", "natural-desc"),
])
.view::<Page>(|binder, _page, section| {
let descriptions = &section.descriptions;
let input = binder.page::<super::Page>().expect("input page not found");
settings::view_section(&section.title)
.add(settings::item(
&descriptions[0],
// TODO show numeric value
// TODO desired range?
widget::slider(
1.0..=100.0,
input
.input_default
.scroll_config
.as_ref()
.and_then(|x| x.scroll_factor)
.unwrap_or(1.)
.log(2.)
* 10.0
+ 50.0,
|value| Message::SetScrollFactor(2f64.powf((value - 50.0) / 10.0), false),
),
))
.add(
settings::item::builder(&descriptions[1])
.description(&descriptions[2])
.toggler(
input
.input_default
.scroll_config
.as_ref()
.and_then(|x| x.natural_scroll)
.unwrap_or(false),
|x| Message::SetNaturalScroll(x, false),
),
)
.apply(Element::from)
.map(crate::pages::Message::Input)
})
}

View file

@ -0,0 +1,142 @@
use apply::Apply;
use cosmic::iced::widget;
use cosmic::widget::settings;
use cosmic::Element;
use cosmic_comp_config::input::AccelProfile;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
use super::Message;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![
sections.insert(touchpad()),
sections.insert(scrolling()),
])
}
fn info(&self) -> page::Info {
page::Info::new("touchpad", "input-touchpad-symbolic")
.title(fl!("touchpad"))
.description(fl!("touchpad", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
fn touchpad() -> Section<crate::pages::Message> {
Section::default()
.descriptions(vec![
fl!("touchpad", "primary-button"),
fl!("touchpad", "speed"),
fl!("touchpad", "acceleration"),
fl!("touchpad", "acceleration-desc"),
fl!("touchpad", "double-click-speed"),
fl!("touchpad", "double-click-speed-desc"),
])
.view::<Page>(|binder, _page, section| {
let descriptions = &section.descriptions;
let input = binder.page::<super::Page>().expect("input page not found");
settings::view_section(&section.title)
.add(settings::item(
&descriptions[0],
cosmic::widget::segmented_selection::horizontal(&input.touchpad_primary_button)
.on_activate(|x| Message::PrimaryButtonSelected(x, true)),
))
.add(
settings::item::builder(&descriptions[1]).control(widget::slider(
0.0..=100.0,
(input
.input_touchpad
.acceleration
.as_ref()
.map_or(0.0, |x| x.speed)
+ 1.0)
* 50.0,
|value| Message::SetMouseSpeed((value / 50.0) - 1.0, true),
)),
)
.add(
settings::item::builder(&descriptions[2])
.description(&descriptions[3])
.toggler(
input
.input_touchpad
.acceleration
.as_ref()
.map_or(true, |x| x.profile == Some(AccelProfile::Adaptive)),
|x| Message::SetAcceleration(x, true),
),
)
// TODO disable while typing
.add(
settings::item::builder(&descriptions[4])
.description(&descriptions[5])
.control(widget::slider(0..=100, 0, |x| {
Message::SetDoubleClickSpeed(x, true)
})),
)
.apply(Element::from)
.map(crate::pages::Message::Input)
})
}
fn scrolling() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("mouse-scrolling"))
.descriptions(vec![
fl!("mouse-scrolling", "speed"),
fl!("mouse-scrolling", "natural"),
fl!("mouse-scrolling", "natural-desc"),
])
.view::<Page>(|binder, _page, section| {
let descriptions = &section.descriptions;
let input = binder.page::<super::Page>().expect("input page not found");
settings::view_section(&section.title)
.add(settings::item(
&descriptions[0],
// TODO show numeric value
// TODO desired range?
widget::slider(
1.0..=100.0,
input
.input_touchpad
.scroll_config
.as_ref()
.and_then(|x| x.scroll_factor)
.unwrap_or(1.)
.log(2.)
* 10.0
+ 50.0,
|value| Message::SetScrollFactor(2f64.powf((value - 50.0) / 10.0), true),
),
))
.add(
settings::item::builder(&descriptions[1])
.description(&descriptions[2])
.toggler(
input
.input_touchpad
.scroll_config
.as_ref()
.and_then(|x| x.natural_scroll)
.unwrap_or(false),
|x| Message::SetNaturalScroll(x, true),
),
)
.apply(Element::from)
.map(crate::pages::Message::Input)
})
}

View file

@ -4,6 +4,7 @@
use cosmic_settings_page::Entity;
pub mod desktop;
pub mod input;
pub mod networking;
pub mod sound;
pub mod system;
@ -17,6 +18,7 @@ pub enum Message {
Panel(desktop::panel::Message),
DesktopWallpaper(desktop::wallpaper::Message),
Applet(desktop::panel::applets::Message),
Input(input::Message),
External { id: String, message: Vec<u8> },
Page(Entity),
}

View file

@ -221,3 +221,60 @@ firmware = Firmware
users = Users
.desc = Authentication and login, lock screen.
## Input
input = Input
.desc = Input
## Input: Keyboard
keyboard = Keyboard
.desc = Keyboard input
keyboard-sources = Input Sources
.desc = Input sources can be switched using Super+Space key combination. This can be customized in the keyboard shortcut settings.
.move-up = Move up
.move-down = Move down
.settings = Settings
.view-layout = View keyboard layout
.remove = Remove
keyboard-special-char = Special Character Entry
.alternate = Alternate characters key
.compose = Compose key
## Input: Keyboard: Shortcuts
keyboard-shortcuts = Keyboard Shortcuts
.desc = View and customize shortcuts
## Input: Mouse
mouse = Mouse
.desc = Mouse speed, acceleration, natural scrolling.
.primary-button = Primary button
.primary-button-left = Left
.primary-button-right = Right
.speed = Mouse speed
.acceleration = Enable mouse acceleration
.acceleration-desc = Automatically adjusts tracking sensitivty based on speed.
.double-click-speed = Double-click speed
.double-click-speed-desc = Changes how fast double-clicks have to be to register.
mouse-scrolling = Scrolling
.speed = Scrolling speed
.natural = Natural scrolling
.natural-desc = Scroll the content, instead of the view
## Input: Touchpad
touchpad = Touchpad
.desc = Touchpad speed, click options, gestures.
.primary-button = Primary button
.primary-button-left = Left
.primary-button-right = Right
.speed = Touchpad speed
.acceleration = Enable touchpad acceleration
.acceleration-desc = Automatically adjusts tracking sensitivty based on speed.
.double-click-speed = Double-click speed
.double-click-speed-desc = Changes how fast double-clicks have to be to register.

View file

@ -118,19 +118,22 @@ impl<Message: 'static> Binder<Message> {
self.page.get_mut(id).map(AsMut::as_mut)
}
/// Get entity ID of page by its type ID.
pub fn page_id<P: Page<Message>>(&self) -> Option<crate::Entity> {
self.typed_page_ids.get(&TypeId::of::<P>()).copied()
}
/// Obtain a reference to a page by its type ID.
#[must_use]
pub fn page<P: Page<Message>>(&self) -> Option<&P> {
let id = self.typed_page_ids.get(&TypeId::of::<P>())?;
let page = self.page.get(*id)?;
let page = self.page.get(self.page_id::<P>()?)?;
page.downcast_ref::<P>()
}
/// Obtain a reference to a page by its type ID.
#[must_use]
pub fn page_mut<P: Page<Message>>(&mut self) -> Option<&mut P> {
let id = self.typed_page_ids.get(&TypeId::of::<P>())?;
let page = self.page.get_mut(*id)?;
let page = self.page.get_mut(self.page_id::<P>()?)?;
page.downcast_mut::<P>()
}