From 2a77cdacb4255089677cf49c06b20d5e17b7303f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 28 Apr 2023 15:45:29 -0700 Subject: [PATCH] Add input settings, with mouse, keyboard, keyboard shortcuts sub-pages Still needs to be integrated with compositor so it actually works, need touchpad page, etc. --- Cargo.lock | 1 + app/Cargo.toml | 1 + app/src/app.rs | 14 +- app/src/pages/input/keyboard/mod.rs | 201 ++++++++++++++++++++++ app/src/pages/input/keyboard/shortcuts.rs | 54 ++++++ app/src/pages/input/mod.rs | 81 +++++++++ app/src/pages/input/mouse.rs | 120 +++++++++++++ app/src/pages/mod.rs | 2 + i18n/en/cosmic_settings.ftl | 46 +++++ page/src/binder.rs | 11 +- 10 files changed, 526 insertions(+), 5 deletions(-) create mode 100644 app/src/pages/input/keyboard/mod.rs create mode 100644 app/src/pages/input/keyboard/shortcuts.rs create mode 100644 app/src/pages/input/mod.rs create mode 100644 app/src/pages/input/mouse.rs diff --git a/Cargo.lock b/Cargo.lock index 1655f24..7ab6c99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -783,6 +783,7 @@ dependencies = [ "cosmic-settings-page", "cosmic-settings-system", "cosmic-settings-time", + "derivative", "derive_setters", "dirs 5.0.1", "downcast-rs", diff --git a/app/Cargo.toml b/app/Cargo.toml index 0bc7e75..56ef6be 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -13,6 +13,7 @@ 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" diff --git a/app/src/app.rs b/app/src/app.rs index 73c3301..20027c6 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -39,7 +39,7 @@ use crate::{ applets::{self, APPLET_DND_ICON_ID}, }, }, - sound, system, time, + input, sound, system, time, }, subscription::desktop_files, widget::{page_title, parent_page_button, search_header, sub_page_button}, @@ -142,6 +142,8 @@ impl Application for SettingsApp { // app.insert_page::(); // app.insert_page::(); + // + app.insert_page::(); let active_id = app .pages @@ -268,6 +270,16 @@ impl Application for SettingsApp { crate::pages::Message::DesktopWallpaper(message) => { page::update!(self.pages, message, desktop::wallpaper::Page); } + crate::pages::Message::Input(message) => { + if matches!(message, input::Message::OpenKeyboardShortcuts) { + if let Some(id) = self.pages.page_id::() { + self.activate_page(id); + } + } + if let Some(page) = self.pages.page_mut::() { + page.update(message); + } + } crate::pages::Message::External { .. } => { todo!("external plugins not supported yet"); } diff --git a/app/src/pages/input/keyboard/mod.rs b/app/src/pages/input/keyboard/mod.rs new file mode 100644 index 0000000..9738578 --- /dev/null +++ b/app/src/pages/input/keyboard/mod.rs @@ -0,0 +1,201 @@ +use apply::Apply; +use cosmic::iced::{ + self, + widget::{self, horizontal_space}, + Length, +}; +use cosmic::iced_style; +use cosmic::widget::settings; +use cosmic_settings_page::Section; +use cosmic_settings_page::{self as page, section}; +use slotmap::SlotMap; + +use super::Message; + +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, +} + +#[derive(Default)] +pub struct Page; + +// 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, + sections: &mut SlotMap>, + ) -> Option { + 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 for Page { + fn sub_pages(page: page::Insert) -> page::Insert { + page.sub_page::() + } +} + +fn input_sources() -> Section { + // TODO desc + Section::default() + .title(fl!("keyboard-sources")) + .view::(|binder, _page, section| { + let input = binder.page::().expect("input page not found"); + + // TODO Need something more custom, with drag and drop + let mut section = settings::view_section(§ion.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 { + Section::default() + .title(fl!("keyboard-special-char")) + .descriptions(vec![ + fl!("keyboard-special-char", "alternate"), + fl!("keyboard-special-char", "compose"), + ]) + .view::(|_binder, _page, section| { + let descriptions = §ion.descriptions; + + // TODO dialogs + settings::view_section(§ion.title) + .add(settings::item(&descriptions[0], go_next_control())) + .add(settings::item(&descriptions[1], go_next_control())) + .apply(cosmic::Element::from) + .map(crate::pages::Message::Input) + }) +} + +fn keyboard_shortcuts() -> Section { + Section::default() + .title(fl!("keyboard-shortcuts")) + .descriptions(vec![fl!("keyboard-shortcuts", "desc")]) + .view::(|binder, _page, section| { + let descriptions = §ion.descriptions; + + settings::view_section(§ion.title) + .add( + settings::item(&descriptions[0], 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(Message::OpenKeyboardShortcuts), + ) + .apply(cosmic::Element::from) + .map(crate::pages::Message::Input) + }) +} + +fn go_next_control() -> cosmic::Element<'static, Message> { + widget::row!( + horizontal_space(Length::Fill), + cosmic::widget::icon("go-next-symbolic", 20).style(cosmic::theme::Svg::Symbolic) + ) + .into() +} diff --git a/app/src/pages/input/keyboard/shortcuts.rs b/app/src/pages/input/keyboard/shortcuts.rs new file mode 100644 index 0000000..e01fe7c --- /dev/null +++ b/app/src/pages/input/keyboard/shortcuts.rs @@ -0,0 +1,54 @@ +use apply::Apply; +use cosmic::iced::{ + widget::{self, horizontal_space}, + Length, +}; +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 for Page { + fn content( + &self, + sections: &mut SlotMap>, + ) -> Option { + 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 for Page {} + +fn shortcuts() -> Section { + Section::default() + .descriptions(vec![]) + .view::(|binder, _page, section| { + let descriptions = §ion.descriptions; + + let input = binder + .page::() + .expect("input page not found"); + + // TODO need something more custom + /* + settings::view_section(§ion.title) + .apply(Element::from) + .map(crate::pages::Message::Input) + */ + widget::column![settings::view_section(§ion.title)] + .apply(Element::from) + .map(crate::pages::Message::Input) + }) +} diff --git a/app/src/pages/input/mod.rs b/app/src/pages/input/mod.rs new file mode 100644 index 0000000..744f65a --- /dev/null +++ b/app/src/pages/input/mod.rs @@ -0,0 +1,81 @@ +use cosmic_settings_page as page; + +pub mod keyboard; +mod mouse; + +#[derive(Clone, Debug)] +pub enum Message { + SetAcceleration(bool), + SetNaturalScroll(bool), + SetScrollSpeed(u32), + SetDoubleClickSpeed(u32), + SetMouseSpeed(u32), + PrimaryButtonSelected(cosmic::widget::segmented_button::Entity), + // seperate close message, to make sure another isn't closed? + ExpandInputSourcePopover(Option), + OpenKeyboardShortcuts, +} + +#[derive(derivative::Derivative)] +#[derivative(Default)] +pub struct Page { + // Mouse + #[derivative(Default(value = "mouse::default_primary_button()"))] + primary_button: cosmic::widget::segmented_button::SingleSelectModel, + acceleration: bool, + natural_scroll: bool, + double_click_speed: u32, + scroll_speed: u32, + mouse_speed: u32, + + // Keyboard + expanded_source_popover: Option, + #[derivative(Default(value = "keyboard::default_input_sources()"))] + sources: Vec, +} + +impl Page { + // TODO + pub fn update(&mut self, message: Message) { + match message { + Message::SetAcceleration(value) => { + self.acceleration = value; + } + Message::SetNaturalScroll(value) => { + self.natural_scroll = value; + } + Message::SetScrollSpeed(value) => { + self.scroll_speed = value; + } + Message::SetDoubleClickSpeed(value) => { + self.double_click_speed = value; + } + Message::SetMouseSpeed(value) => { + self.mouse_speed = value; + } + Message::PrimaryButtonSelected(entity) => { + self.primary_button.activate(entity); + } + Message::ExpandInputSourcePopover(value) => { + self.expanded_source_popover = value; + } + // TODO Specially handled in app.rs + Message::OpenKeyboardShortcuts => {} + } + } +} + +impl page::Page 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 for Page { + fn sub_pages(page: page::Insert) -> page::Insert { + page.sub_page::().sub_page::() + } +} diff --git a/app/src/pages/input/mouse.rs b/app/src/pages/input/mouse.rs new file mode 100644 index 0000000..fd23251 --- /dev/null +++ b/app/src/pages/input/mouse.rs @@ -0,0 +1,120 @@ +use apply::Apply; +use cosmic::iced::{ + widget::{self, horizontal_space}, + Length, +}; +use cosmic::widget::settings; +use cosmic::Element; +use cosmic_settings_page::Section; +use cosmic_settings_page::{self as page, section}; +use slotmap::SlotMap; + +use super::Message; + +// XXX +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 for Page { + fn content( + &self, + sections: &mut SlotMap>, + ) -> Option { + 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 for Page {} + +fn mouse() -> Section { + 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::(|binder, _page, section| { + let descriptions = §ion.descriptions; + + let input = binder.page::().expect("input page not found"); + + // TODO need something more custom + settings::view_section(§ion.title) + // TODO + .add(settings::item( + &descriptions[0], + cosmic::widget::segmented_selection::horizontal(&input.primary_button) + .on_activate(Message::PrimaryButtonSelected), + )) + .add( + settings::item::builder(&descriptions[1]).control(widget::slider( + 0..=100, + input.mouse_speed, + Message::SetMouseSpeed, + )), + ) + .add( + settings::item::builder(&descriptions[2]) + .description(&descriptions[3]) + .toggler(input.acceleration, Message::SetAcceleration), + ) + .add( + settings::item::builder(&descriptions[4]) + .description(&descriptions[5]) + .control(widget::slider( + 0..=100, + input.double_click_speed, + Message::SetDoubleClickSpeed, + )), + ) + .apply(Element::from) + .map(crate::pages::Message::Input) + }) +} + +fn scrolling() -> Section { + Section::default() + .title(fl!("mouse-scrolling")) + .descriptions(vec![ + fl!("mouse-scrolling", "speed"), + fl!("mouse-scrolling", "natural"), + fl!("mouse-scrolling", "natural-desc"), + ]) + .view::(|binder, _page, section| { + let descriptions = §ion.descriptions; + + let input = binder.page::().expect("input page not found"); + + settings::view_section(§ion.title) + .add(settings::item( + &descriptions[0], + // TODO show numeric value + widget::slider(0..=100, input.scroll_speed, Message::SetScrollSpeed), + )) + .add( + settings::item::builder(&descriptions[1]) + .description(&descriptions[2]) + .toggler(input.natural_scroll, Message::SetNaturalScroll), + ) + .apply(Element::from) + .map(crate::pages::Message::Input) + }) +} diff --git a/app/src/pages/mod.rs b/app/src/pages/mod.rs index 019986e..c265e0f 100644 --- a/app/src/pages/mod.rs +++ b/app/src/pages/mod.rs @@ -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 }, Page(Entity), } diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 99f5eef..b7e7aa4 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -221,3 +221,49 @@ 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 diff --git a/page/src/binder.rs b/page/src/binder.rs index ace3e93..1947a29 100644 --- a/page/src/binder.rs +++ b/page/src/binder.rs @@ -118,19 +118,22 @@ impl Binder { self.page.get_mut(id).map(AsMut::as_mut) } + /// Get entity ID of page by its type ID. + pub fn page_id>(&self) -> Option { + self.typed_page_ids.get(&TypeId::of::

()).copied() + } + /// Obtain a reference to a page by its type ID. #[must_use] pub fn page>(&self) -> Option<&P> { - let id = self.typed_page_ids.get(&TypeId::of::

())?; - let page = self.page.get(*id)?; + let page = self.page.get(self.page_id::

()?)?; page.downcast_ref::

() } /// Obtain a reference to a page by its type ID. #[must_use] pub fn page_mut>(&mut self) -> Option<&mut P> { - let id = self.typed_page_ids.get(&TypeId::of::

())?; - let page = self.page.get_mut(*id)?; + let page = self.page.get_mut(self.page_id::

()?)?; page.downcast_mut::

() }