Skip to content

Commit

Permalink
Started adding a more reactive shortcut listener
Browse files Browse the repository at this point in the history
  • Loading branch information
samclane committed Oct 3, 2024
1 parent cba13b0 commit e5d025c
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 33 deletions.
6 changes: 3 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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() {
Expand Down
8 changes: 7 additions & 1 deletion src/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InputItem>);

impl Display for InputAction {
Expand All @@ -136,6 +136,12 @@ impl Display for InputAction {
}
}

impl From<BTreeSet<InputItem>> for InputAction {
fn from(items: BTreeSet<InputItem>) -> Self {
InputAction(items)
}
}

#[derive(Debug)]
pub enum InputActionParseError {
InvalidItem(String),
Expand Down
130 changes: 101 additions & 29 deletions src/shortcut.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Fn(BTreeSet<InputItem>) + Send + Sync + 'static>;
pub type ShortcutCallback = Arc<dyn Fn(InputAction) + Send + Sync + 'static>;

#[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct KeyboardShortcut {
pub keys: BTreeSet<InputItem>,
pub keys: InputAction,
display_name: String,
}

impl KeyboardShortcut {
pub fn new(keys: BTreeSet<InputItem>, 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<InputItem>) -> bool {
self.keys.is_subset(keys_pressed)
fn is_matched(&self, keys_pressed: &InputAction) -> bool {
self.keys.0.is_subset(&keys_pressed.0)
}
}

Expand All @@ -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;
Expand All @@ -55,10 +57,10 @@ impl TextBuffer for KeyboardShortcut {
}

fn delete_char_range(&mut self, char_range: std::ops::Range<usize>) {
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();
Expand All @@ -67,14 +69,14 @@ impl TextBuffer for KeyboardShortcut {

impl Debug for KeyboardShortcut {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let keys: Vec<String> = self.keys.iter().map(|k| format!("{}", k)).collect();
let keys: Vec<String> = 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<String> = self.keys.iter().map(|k| format!("{}", k)).collect();
let keys: Vec<String> = self.keys.0.iter().map(|k| format!("{}", k)).collect();
write!(f, "{}", keys.join(" + "))
}
}
Expand All @@ -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| {}),
Expand All @@ -112,7 +114,7 @@ impl ShortcutManager {

pub fn add_shortcut<F>(&self, action_name: String, shortcut: KeyboardShortcut, callback: F)
where
F: Fn(BTreeSet<InputItem>) + Send + Sync + 'static,
F: Fn(InputAction) + Send + Sync + 'static,
{
let arc_callback: ShortcutCallback = Arc::new(callback);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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| {}),
Expand All @@ -202,49 +205,116 @@ 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]
fn test_keyboard_shortcut_display() {
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"));
Expand All @@ -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);

Expand All @@ -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));
}

Expand All @@ -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(),
Expand Down

0 comments on commit e5d025c

Please sign in to comment.