From 004fd617eab718ccb9cfb4f159b605711f0fd3f3 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 29 Jan 2024 12:21:54 -0700 Subject: [PATCH] Show key bindings in menu --- src/key_bind.rs | 68 +++++++++++++++++++++++++++ src/main.rs | 120 +++++++++++++++++++----------------------------- src/menu.rs | 46 +++++++++++-------- 3 files changed, 140 insertions(+), 94 deletions(-) create mode 100644 src/key_bind.rs diff --git a/src/key_bind.rs b/src/key_bind.rs new file mode 100644 index 0000000..25a536d --- /dev/null +++ b/src/key_bind.rs @@ -0,0 +1,68 @@ +use cosmic::iced::keyboard::{KeyCode, Modifiers}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt}; + +use crate::Action; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub enum Modifier { + Super, + Ctrl, + Alt, + Shift, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct KeyBind { + pub modifiers: Vec, + pub key_code: KeyCode, +} + +impl KeyBind { + pub fn matches(&self, modifiers: Modifiers, key_code: KeyCode) -> bool { + self.key_code == key_code + && modifiers.logo() == self.modifiers.contains(&Modifier::Super) + && modifiers.control() == self.modifiers.contains(&Modifier::Ctrl) + && modifiers.alt() == self.modifiers.contains(&Modifier::Alt) + && modifiers.shift() == self.modifiers.contains(&Modifier::Shift) + } +} + +impl fmt::Display for KeyBind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for modifier in self.modifiers.iter() { + write!(f, "{:?} + ", modifier)?; + } + write!(f, "{:?}", self.key_code) + } +} + +//TODO: load from config +pub fn key_binds() -> HashMap { + let mut key_binds = HashMap::new(); + + macro_rules! bind { + ([$($modifier:ident),+ $(,)?], $key_code:ident, $action:ident) => {{ + key_binds.insert( + KeyBind { + modifiers: vec![$(Modifier::$modifier),+], + key_code: KeyCode::$key_code, + }, + Action::$action, + ); + }}; + } + + bind!([Ctrl], C, Copy); + bind!([Ctrl], X, Cut); + bind!([Ctrl], V, Paste); + bind!([Ctrl], A, SelectAll); + bind!([Ctrl], W, TabClose); + bind!([Ctrl], T, TabNew); + bind!([Ctrl], Tab, TabNext); + bind!([Ctrl, Shift], Tab, TabPrev); + bind!([Ctrl], Q, WindowClose); + bind!([Ctrl], N, WindowNew); + + key_binds +} diff --git a/src/main.rs b/src/main.rs index 15f71fe..e8aac8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,11 +15,14 @@ use cosmic::{ widget::{self, segmented_button}, Application, ApplicationExt, Element, }; -use std::{any::TypeId, env, fs, path::PathBuf, process}; +use std::{any::TypeId, env, fs, path::PathBuf, process, collections::HashMap}; use config::{AppTheme, Config, CONFIG_VERSION}; mod config; +use key_bind::{key_binds, KeyBind}; +mod key_bind; + mod localize; mod menu; @@ -104,7 +107,7 @@ pub struct Flags { config: Config, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { Copy, Cut, @@ -116,23 +119,37 @@ pub enum Action { RestoreFromTrash, SelectAll, Settings, + TabClose, TabNew, + TabNext, + TabPrev, + TabViewGrid, + TabViewList, + WindowClose, + WindowNew, } impl Action { - pub fn message(self, entity: segmented_button::Entity) -> Message { + pub fn message(self, entity_opt: Option) -> Message { match self { - Action::Copy => Message::Copy(Some(entity)), - Action::Cut => Message::Cut(Some(entity)), - Action::MoveToTrash => Message::MoveToTrash(Some(entity)), - Action::NewFile => Message::NewFile(Some(entity)), - Action::NewFolder => Message::NewFolder(Some(entity)), - Action::Paste => Message::Paste(Some(entity)), + Action::Copy => Message::Copy(entity_opt), + Action::Cut => Message::Cut(entity_opt), + Action::MoveToTrash => Message::MoveToTrash(entity_opt), + Action::NewFile => Message::NewFile(entity_opt), + Action::NewFolder => Message::NewFolder(entity_opt), + Action::Paste => Message::Paste(entity_opt), Action::Properties => Message::ToggleContextPage(ContextPage::Properties), - Action::RestoreFromTrash => Message::RestoreFromTrash(Some(entity)), - Action::SelectAll => Message::SelectAll(Some(entity)), + Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), + Action::SelectAll => Message::SelectAll(entity_opt), Action::Settings => Message::ToggleContextPage(ContextPage::Settings), + Action::TabClose => Message::TabClose(entity_opt), Action::TabNew => Message::TabNew, + Action::TabNext => Message::TabNext, + Action::TabPrev => Message::TabPrev, + Action::TabViewGrid => Message::TabMessage(entity_opt, tab::Message::View(tab::View::Grid)), + Action::TabViewList => Message::TabMessage(entity_opt, tab::Message::View(tab::View::List)), + Action::WindowClose => Message::WindowClose, + Action::WindowNew => Message::WindowNew, } } } @@ -145,7 +162,8 @@ pub enum Message { Config(Config), Copy(Option), Cut(Option), - KeyModifiers(Modifiers), + Key(Modifiers, KeyCode), + Modifiers(Modifiers), MoveToTrash(Option), NewFile(Option), NewFolder(Option), @@ -191,6 +209,7 @@ pub struct App { config: Config, app_themes: Vec, context_page: ContextPage, + key_binds: HashMap, modifiers: Modifiers, } @@ -367,6 +386,7 @@ impl Application for App { config: flags.config, app_themes, context_page: ContextPage::Settings, + key_binds: key_binds(), modifiers: Modifiers::empty(), }; @@ -475,7 +495,15 @@ impl Application for App { Message::Cut(entity_opt) => { log::warn!("TODO: CUT"); } - Message::KeyModifiers(modifiers) => { + Message::Key(modifiers, key_code) => { + let entity = self.tab_model.active(); + for (key_bind, action) in self.key_binds.iter() { + if key_bind.matches(modifiers, key_code) { + return self.update(action.message(Some(entity))); + } + } + } + Message::Modifiers(modifiers) => { self.modifiers = modifiers; } Message::MoveToTrash(entity_opt) => { @@ -578,7 +606,7 @@ impl Application for App { tab.context_menu = None; } // Run action's message - return self.update(action.message(entity)); + return self.update(action.message(Some(entity))); } _ => {} } @@ -671,7 +699,7 @@ impl Application for App { fn header_start(&self) -> Vec> { vec![ - menu::menu_bar().into(), + menu::menu_bar(&self.key_binds).into(), //TODO: use theme defined space? widget::horizontal_space(Length::Fixed(32.0)).into(), ] @@ -753,69 +781,13 @@ impl Application for App { Subscription::batch([ event::listen_with(|event, _status| match event { Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::A, + key_code, modifiers, }) => { - if modifiers == Modifiers::CTRL { - Some(Message::SelectAll(None)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::C, - modifiers, - }) => { - if modifiers == Modifiers::CTRL { - Some(Message::Copy(None)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::X, - modifiers, - }) => { - if modifiers == Modifiers::CTRL { - Some(Message::Cut(None)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::T, - modifiers, - }) => { - if modifiers == Modifiers::CTRL { - Some(Message::TabNew) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::W, - modifiers: Modifiers::CTRL, - }) => Some(Message::TabClose(None)), - Event::Keyboard(KeyEvent::KeyPressed { - key_code: key @ (KeyCode::PageUp | KeyCode::PageDown), - modifiers: Modifiers::CTRL, - }) => match key { - KeyCode::PageDown => Some(Message::TabPrev), - KeyCode::PageUp => Some(Message::TabNext), - _ => None, - }, - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::V, - modifiers, - }) => { - if modifiers == Modifiers::CTRL { - Some(Message::Paste(None)) - } else { - None - } + Some(Message::Key(modifiers, key_code)) } Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { - Some(Message::KeyModifiers(modifiers)) + Some(Message::Modifiers(modifiers)) } _ => None, }), diff --git a/src/menu.rs b/src/menu.rs index 7362223..3281620 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -11,8 +11,9 @@ use cosmic::{ }, Element, }; +use std::collections::HashMap; -use crate::{fl, tab, Action, ContextPage, Location, Message, Tab}; +use crate::{fl, KeyBind, tab, Action, ContextPage, Location, Message, Tab}; macro_rules! menu_button { ($($x:expr),+ $(,)?) => ( @@ -30,6 +31,7 @@ macro_rules! menu_button { } pub fn context_menu<'a>(entity: segmented_button::Entity, tab: &Tab) -> Element<'a, Message> { + //TODO: show key bindings in context menu? let menu_action = |label, action| { menu_button!(widget::text(label)).on_press(Message::TabContextAction(entity, action)) }; @@ -87,7 +89,7 @@ pub fn context_menu<'a>(entity: segmented_button::Entity, tab: &Tab) -> Element< .into() } -pub fn menu_bar<'a>() -> Element<'a, Message> { +pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message> { //TODO: port to libcosmic let menu_root = |label| { widget::button(widget::text(label)) @@ -95,20 +97,24 @@ pub fn menu_bar<'a>() -> Element<'a, Message> { .style(theme::Button::MenuRoot) }; - let find_key = |message: &Message| -> String { - //TODO: hotkey config + let find_key = |action: &Action| -> String { + for (key_bind, key_action) in key_binds.iter() { + if action == key_action { + return key_bind.to_string(); + } + } String::new() }; - let menu_item = |label, message| { - let key = find_key(&message); + let menu_item = |label, action| { + let key = find_key(&action); MenuTree::new( menu_button!( widget::text(label), widget::horizontal_space(Length::Fill), widget::text(key) ) - .on_press(message), + .on_press(action.message(None)), ) }; @@ -116,23 +122,23 @@ pub fn menu_bar<'a>() -> Element<'a, Message> { MenuTree::with_children( menu_root(fl!("file")), vec![ - menu_item(fl!("new-tab"), Message::TabNew), - menu_item(fl!("new-window"), Message::WindowNew), - menu_item(fl!("new-file"), Message::NewFile(None)), - menu_item(fl!("new-folder"), Message::NewFolder(None)), + menu_item(fl!("new-tab"), Action::TabNew), + menu_item(fl!("new-window"), Action::WindowNew), + menu_item(fl!("new-file"), Action::NewFile), + menu_item(fl!("new-folder"), Action::NewFolder), MenuTree::new(horizontal_rule(1)), - menu_item(fl!("close-tab"), Message::TabClose(None)), + menu_item(fl!("close-tab"), Action::TabClose), MenuTree::new(horizontal_rule(1)), - menu_item(fl!("quit"), Message::WindowClose), + menu_item(fl!("quit"), Action::WindowClose), ], ), MenuTree::with_children( menu_root(fl!("edit")), vec![ - menu_item(fl!("cut"), Message::Cut(None)), - menu_item(fl!("copy"), Message::Copy(None)), - menu_item(fl!("paste"), Message::Paste(None)), - menu_item(fl!("select-all"), Message::SelectAll(None)), + menu_item(fl!("cut"), Action::Cut), + menu_item(fl!("copy"), Action::Copy), + menu_item(fl!("paste"), Action::Paste), + menu_item(fl!("select-all"), Action::SelectAll), ], ), MenuTree::with_children( @@ -140,16 +146,16 @@ pub fn menu_bar<'a>() -> Element<'a, Message> { vec![ menu_item( fl!("grid-view"), - Message::TabMessage(None, tab::Message::View(tab::View::Grid)), + Action::TabViewGrid ), menu_item( fl!("list-view"), - Message::TabMessage(None, tab::Message::View(tab::View::List)), + Action::TabViewList ), MenuTree::new(horizontal_rule(1)), menu_item( fl!("menu-settings"), - Message::ToggleContextPage(ContextPage::Settings), + Action::Settings, ), ], ),