diff --git a/src/app.rs b/src/app.rs index ab6d04c..6378f51 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,13 +15,13 @@ use crate::{ listener::InputListener, products::TemperatureRange, screencap::{FollowType, ScreenSubregion}, - shortcut::ShortcutManager, + shortcut::{ShortcutEdit, ShortcutManager}, toggle_button, ui::{handle_eyedropper, handle_screencap}, BulbInfo, LifxManager, ScreencapManager, }; -use eframe::egui::{self, Color32, Modifiers, RichText, Ui, Vec2}; +use eframe::egui::{self, Color32, Modifiers, RichText, Ui, Vec2, Widget}; use lifx_core::HSBK; use serde::{Deserialize, Serialize}; @@ -431,7 +431,7 @@ impl MantleApp { &mut self.shortcut_manager.new_shortcut.callback_name, ); ui.separator(); - ui.text_edit_singleline(&mut self.shortcut_manager.new_shortcut.shortcut); + ShortcutEdit::new(&mut self.shortcut_manager.new_shortcut.shortcut).ui(ui); ui.separator(); ui.label("TODO: Add callback"); if ui.button("Add").clicked() { diff --git a/src/listener.rs b/src/listener.rs index e47fe80..53b34eb 100644 --- a/src/listener.rs +++ b/src/listener.rs @@ -125,7 +125,7 @@ impl Ord for InputItem { } } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct InputAction(pub BTreeSet); impl Display for InputAction { @@ -136,6 +136,12 @@ impl Display for InputAction { } } +impl From> for InputAction { + fn from(items: BTreeSet) -> Self { + InputAction(items) + } +} + #[derive(Debug)] pub enum InputActionParseError { InvalidItem(String), diff --git a/src/shortcut.rs b/src/shortcut.rs index 289c888..c72cabd 100644 --- a/src/shortcut.rs +++ b/src/shortcut.rs @@ -1,32 +1,33 @@ -use crate::listener::{InputItem, InputListener}; -use eframe::egui::TextBuffer; +use crate::listener::{from_egui, InputAction, InputListener}; +use eframe::egui::{vec2, Response, Sense, TextBuffer, Ui, Widget}; use std::collections::BTreeSet; use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::str::FromStr; use std::sync::{Arc, Mutex}; -pub type ShortcutCallback = Arc) + Send + Sync + 'static>; +pub type ShortcutCallback = Arc; #[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct KeyboardShortcut { - pub keys: BTreeSet, + pub keys: InputAction, display_name: String, } impl KeyboardShortcut { - pub fn new(keys: BTreeSet, display_name: String) -> Self { + pub fn new(keys: InputAction, display_name: String) -> Self { KeyboardShortcut { keys, display_name } } fn update_display_string(&mut self) { self.display_name = self .keys + .0 .iter() .fold(String::new(), |acc, key| acc + &format!("{} + ", key)); } - fn is_matched(&self, keys_pressed: &BTreeSet) -> bool { - self.keys.is_subset(keys_pressed) + fn is_matched(&self, keys_pressed: &InputAction) -> bool { + self.keys.0.is_subset(&keys_pressed.0) } } @@ -45,8 +46,9 @@ impl TextBuffer for KeyboardShortcut { if offset != char_index { continue; } - if let Ok(key) = InputItem::from_str(&c.to_string()) { - new_keys.insert(key); + if let Ok(keys) = InputAction::from_str(&c.to_string()) { + // combine the 2 sets + new_keys.0.extend(keys.0); } } self.keys = new_keys; @@ -55,10 +57,10 @@ impl TextBuffer for KeyboardShortcut { } fn delete_char_range(&mut self, char_range: std::ops::Range) { - let keys_vec: Vec<_> = self.keys.iter().cloned().collect(); + let keys_vec: Vec<_> = self.keys.0.iter().cloned().collect(); for i in char_range { if let Some(key) = keys_vec.get(i) { - self.keys.remove(key); + self.keys.0.remove(key); } } self.update_display_string(); @@ -67,14 +69,14 @@ impl TextBuffer for KeyboardShortcut { impl Debug for KeyboardShortcut { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - let keys: Vec = self.keys.iter().map(|k| format!("{}", k)).collect(); + let keys: Vec = self.keys.0.iter().map(|k| format!("{}", k)).collect(); write!(f, "KeyboardShortcut({})", keys.join(" + ")) } } impl Display for KeyboardShortcut { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - let keys: Vec = self.keys.iter().map(|k| format!("{}", k)).collect(); + let keys: Vec = self.keys.0.iter().map(|k| format!("{}", k)).collect(); write!(f, "{}", keys.join(" + ")) } } @@ -101,7 +103,7 @@ impl ShortcutManager { active_shortcuts: Arc::new(Mutex::new(BTreeSet::new())), new_shortcut: KeyboardShortcutCallback { shortcut: KeyboardShortcut { - keys: BTreeSet::new(), + keys: InputAction::default(), display_name: "".to_string(), }, callback: Arc::new(|_keys_pressed| {}), @@ -112,7 +114,7 @@ impl ShortcutManager { pub fn add_shortcut(&self, action_name: String, shortcut: KeyboardShortcut, callback: F) where - F: Fn(BTreeSet) + Send + Sync + 'static, + F: Fn(InputAction) + Send + Sync + 'static, { let arc_callback: ShortcutCallback = Arc::new(callback); @@ -148,7 +150,8 @@ impl ShortcutManager { // Register a background callback with the InputListener let input_listener_clone = input_listener.clone(); self.input_listener.add_callback(Box::new(move |_event| { - let keys_pressed = input_listener_clone.get_keys_pressed(); + // cast keys_pressed to InputAction + let keys_pressed = InputAction::from(input_listener_clone.get_keys_pressed()); let mut active_shortcuts_guard = match active_shortcuts.lock() { Ok(guard) => guard, @@ -192,7 +195,7 @@ impl Default for ShortcutManager { active_shortcuts: Arc::new(Mutex::new(BTreeSet::new())), new_shortcut: KeyboardShortcutCallback { shortcut: KeyboardShortcut { - keys: BTreeSet::new(), + keys: InputAction::default(), display_name: "".to_string(), }, callback: Arc::new(|_keys_pressed| {}), @@ -202,41 +205,107 @@ impl Default for ShortcutManager { } } +pub struct ShortcutEdit<'a> { + shortcut: &'a mut KeyboardShortcut, +} + +impl<'a> ShortcutEdit<'a> { + pub fn new(shortcut: &'a mut KeyboardShortcut) -> Self { + Self { shortcut } + } +} + +impl<'a> Widget for ShortcutEdit<'a> { + fn ui(self, ui: &mut Ui) -> Response { + let ShortcutEdit { shortcut } = self; + + // Allocate space for the widget + let desired_size = ui.spacing().interact_size.y * vec2(1.0, 1.0); + let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); + + // Handle focus + if response.clicked() { + response.request_focus(); + } + + let is_focused = response.has_focus(); + + // Draw background + let bg_color = if is_focused { + ui.visuals().selection.bg_fill + } else { + ui.visuals().widgets.inactive.bg_fill + }; + ui.painter().rect_filled(rect, 0.0, bg_color); + + // Handle key input when focused + if is_focused { + ui.input(|inputstate: &eframe::egui::InputState| { + let mut keys_pressed = InputAction::default(); + for key in inputstate.keys_down.iter() { + let modifiers = inputstate.modifiers; + let input_item = from_egui(*key, modifiers); + keys_pressed.0.extend(input_item.0); + } + shortcut.keys = keys_pressed; + shortcut.update_display_string(); + }); + } + + // Display the current shortcut + // let text_style = TextStyle::Button; + // let galley = ui.fonts(|_|{}).layout_single_line( + // text_style.resolve(ui.style()), + // shortcut.display_string.clone(), + // ); + // let text_pos = rect.center() - galley.size() * 0.5; + // ui.painter().galley(text_pos, galley); + let text = shortcut.display_name.clone(); + ui.label(text); + + response + } +} + #[cfg(test)] mod tests { use super::*; - use crate::listener::{InputItem, InputListener}; + use crate::listener::{InputAction, InputItem, InputListener}; use rdev::Key; use std::sync::atomic::{AtomicBool, Ordering}; #[test] fn test_keyboard_shortcut_new() { let keys: BTreeSet<_> = vec![InputItem::Key(Key::KeyA)].into_iter().collect(); - let shortcut = KeyboardShortcut::new(keys.clone(), "TestAction".to_string()); - assert_eq!(shortcut.keys, keys); + let shortcut = + KeyboardShortcut::new(InputAction::from(keys.clone()), "TestAction".to_string()); + assert_eq!(shortcut.keys, InputAction::from(keys.clone())); } #[test] fn test_keyboard_shortcut_is_matched() { let shortcut_keys: BTreeSet<_> = - vec![InputItem::Key(Key::ControlLeft), InputItem::Key(Key::KeyC)] + vec![InputItem::Key(Key::ControlLeft), InputItem::Key(Key::KeyA)] .into_iter() .collect(); - let shortcut = KeyboardShortcut::new(shortcut_keys.clone(), "TestAction".to_string()); + let shortcut = KeyboardShortcut::new( + InputAction::from(shortcut_keys.clone()), + "TestAction".to_string(), + ); // Test with matching keys_pressed let keys_pressed = shortcut_keys.clone(); - assert!(shortcut.is_matched(&keys_pressed)); + assert!(shortcut.is_matched(&InputAction::from(keys_pressed.clone()))); // Test with extra keys in keys_pressed let mut keys_pressed_extra = keys_pressed.clone(); keys_pressed_extra.insert(InputItem::Key(Key::ShiftLeft)); - assert!(shortcut.is_matched(&keys_pressed_extra)); + assert!(shortcut.is_matched(&InputAction::from(keys_pressed_extra.clone()))); // Test with missing keys in keys_pressed let keys_pressed_missing: BTreeSet<_> = vec![InputItem::Key(Key::ControlLeft)].into_iter().collect(); - assert!(!shortcut.is_matched(&keys_pressed_missing)); + assert!(!shortcut.is_matched(&InputAction::from(keys_pressed_missing.clone()))); } #[test] @@ -244,7 +313,8 @@ mod tests { let keys: BTreeSet<_> = vec![InputItem::Key(Key::ControlLeft), InputItem::Key(Key::KeyA)] .into_iter() .collect(); - let shortcut = KeyboardShortcut::new(keys, "TestAction".to_string()); + let shortcut = + KeyboardShortcut::new(InputAction::from(keys.clone()), "TestAction".to_string()); let display_str = format!("{}", shortcut); assert!(display_str.contains("ControlLeft")); assert!(display_str.contains("KeyA")); @@ -253,7 +323,8 @@ mod tests { #[test] fn test_keyboard_shortcut_callback_creation() { let keys: BTreeSet<_> = vec![InputItem::Key(Key::KeyA)].into_iter().collect(); - let shortcut = KeyboardShortcut::new(keys, "TestAction".to_string()); + let shortcut = + KeyboardShortcut::new(InputAction::from(keys.clone()), "TestAction".to_string()); let callback_called = Arc::new(AtomicBool::new(false)); let callback_called_clone = Arc::clone(&callback_called); @@ -268,7 +339,7 @@ mod tests { }; // Simulate calling the callback - (shortcut_callback.callback)(BTreeSet::new()); + (shortcut_callback.callback)(InputAction::from(keys.clone())); assert!(callback_called.load(Ordering::SeqCst)); } @@ -277,7 +348,8 @@ mod tests { let input_listener = InputListener::new(); let shortcut_manager = ShortcutManager::new(input_listener); let keys: BTreeSet<_> = vec![InputItem::Key(Key::KeyA)].into_iter().collect(); - let shortcut = KeyboardShortcut::new(keys.clone(), "TestAction".to_string()); + let shortcut = + KeyboardShortcut::new(InputAction::from(keys.clone()), "TestAction".to_string()); shortcut_manager.add_shortcut( "TestAction".to_string(),