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.
This commit is contained in:
Ian Douglas Scott 2023-04-28 15:45:29 -07:00
parent a4c567a954
commit 2a77cdacb4
10 changed files with 526 additions and 5 deletions

1
Cargo.lock generated
View file

@ -783,6 +783,7 @@ dependencies = [
"cosmic-settings-page",
"cosmic-settings-system",
"cosmic-settings-time",
"derivative",
"derive_setters",
"dirs 5.0.1",
"downcast-rs",

View file

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

View file

@ -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::<accessibility::Page>();
// app.insert_page::<applications::Page>();
//
app.insert_page::<input::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::<input::keyboard::shortcuts::Page>() {
self.activate_page(id);
}
}
if let Some(page) = self.pages.page_mut::<input::Page>() {
page.update(message);
}
}
crate::pages::Message::External { .. } => {
todo!("external plugins not supported yet");
}

View file

@ -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<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(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<crate::pages::Message> {
Section::default()
.title(fl!("keyboard-shortcuts"))
.descriptions(vec![fl!("keyboard-shortcuts", "desc")])
.view::<Page>(|binder, _page, section| {
let descriptions = &section.descriptions;
settings::view_section(&section.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()
}

View file

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

View file

@ -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<String>),
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<String>,
#[derivative(Default(value = "keyboard::default_input_sources()"))]
sources: Vec<keyboard::InputSource>,
}
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<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>()
}
}

View file

@ -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<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");
// TODO need something more custom
settings::view_section(&section.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<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
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)
})
}

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

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>()
}