use apply::Apply; use cosmic::{ iced::{ self, widget::{self, horizontal_space}, window, Length, }, iced_style, theme, widget::{button, container, icon, radio, settings}, }; use cosmic_settings_page::{self as page, section, Section}; use once_cell::sync::Lazy; use slotmap::SlotMap; use super::Message; pub static ADD_INPUT_SOURCE_DIALOGUE_ID: Lazy = Lazy::new(|| window::Id::unique()); pub static SPECIAL_CHARACTER_DIALOGUE_ID: Lazy = Lazy::new(|| window::Id::unique()); 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::style(theme) } })) .apply(button) .style(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| { let cosmic = theme.cosmic(); container::Appearance { icon_color: Some(theme.cosmic().background.on.into()), text_color: Some(theme.cosmic().background.on.into()), background: Some(iced::Color::from(theme.cosmic().background.base).into()), border_radius: cosmic.corner_radii.radius_m.into(), border_width: 0.0, border_color: iced::Color::TRANSPARENT, } })) .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()) }); let button = button::icon(icon::from_name("open-menu-symbolic")) .extra_small() .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![radio(desc, value, Some(current_value), |_| { Message::SpecialCharacterSelect(value) }) .into()]) .into() } #[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(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 { Section::default() .title(fl!("keyboard-shortcuts")) .descriptions(vec![fl!("keyboard-shortcuts", "desc")]) .view::(|binder, _page, section| { let descriptions = §ion.descriptions; let mut section = settings::view_section(§ion.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() -> cosmic::Element<'static, Msg> { widget::row!( horizontal_space(Length::Fill), icon::from_name("go-next-symbolic").size(16).icon(), ) .into() } fn go_next_item(description: &str, msg: Msg) -> cosmic::Element<'_, Msg> { settings::item(description, go_next_control()) .apply(widget::container) .style(cosmic::theme::Container::custom( cosmic::widget::list::style, )) .apply(button) .style(theme::Button::Transparent) .on_press(msg) .into() }