Files
virtual-controller/plugin-reaper-relearn/main/src/domain/key_source.rs
T
Paul Lipscomb 7ecc718f5d Reorganize top-level directories with clearer naming convention
app -> app-desktop-macos, presets -> app-presets, server -> remote-server,
daw-config-reaper -> osc-config-daw, plugin-reaper-realearn -> plugin-reaper-relearn.
Updated run.py and presets.py path references accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 17:51:05 -04:00

330 lines
12 KiB
Rust

use crate::domain::ControlOutcome;
use enumflags2::BitFlags;
use helgoboss_learn::{ControlValue, UnitValue};
use reaper_high::{AcceleratorKey, Reaper};
use reaper_medium::{
virt_keys, Accel, AccelMsgKind, AcceleratorBehavior, AcceleratorKeyCode, ReaperString, VirtKey,
};
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct KeySource {
currently_pressed: bool,
stroke: Keystroke,
}
impl KeySource {
pub fn new(stroke: Keystroke) -> Self {
Self {
currently_pressed: false,
stroke,
}
}
pub fn stroke(&self) -> Keystroke {
self.stroke
}
pub fn control(&mut self, msg: KeyMessage) -> Option<ControlOutcome<ControlValue>> {
if !(msg.stroke() == self.stroke) {
// If strokes don't match, we can return early. All tests below assume that the stroke matches.
return None;
}
if !msg.interaction_kind().is_press_or_release() {
// On Windows, there's not just press and release but also something like "key is being
// hold", which fires continuously. We neither want to react to it (because we have our
// own fire modes) nor simply forward it to REAPER (because it would dig a hole
// into our "Filter matched events" mechanism). We let this source "consume" the message
// instead.
// Oh yes, and there's "Char". If in a text field, Windows (and maybe also other OS?)
// sends for each character key press an additional "Char" interaction. It should have
// been normalized in the accelerator and match the keystroke of the key-down event.
// As a result, we consume it as well.
return Some(ControlOutcome::Consumed);
}
let is_press = msg.interaction_kind().is_press();
if is_press && self.currently_pressed {
// We don't want OS-triggered repeated key firing (macOS). We have our own fire modes.
return Some(ControlOutcome::Consumed);
}
let control_value = self.get_control_value(msg)?;
self.currently_pressed = is_press;
Some(ControlOutcome::Matched(control_value))
}
/// Non-mutating! Used for checks.
pub fn reacts_to_message_with(&self, msg: KeyMessage) -> Option<ControlValue> {
if !msg.interaction_kind().is_press_or_release() {
return None;
}
self.get_control_value(msg)
}
/// Assumes that relevance has been checked already.
fn get_control_value(&self, msg: KeyMessage) -> Option<ControlValue> {
if msg.stroke != self.stroke {
return None;
}
let value = if msg.interaction_kind().is_press() {
UnitValue::MAX
} else {
UnitValue::MIN
};
Some(ControlValue::AbsoluteContinuous(value))
}
}
impl Display for KeySource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.stroke.fmt(f)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct KeyMessage {
kind: AccelMsgKind,
stroke: Keystroke,
}
impl KeyMessage {
pub fn new(kind: AccelMsgKind, stroke: Keystroke) -> Self {
Self { kind, stroke }
}
pub fn interaction_kind(&self) -> KeyInteractionKind {
use AccelMsgKind::*;
match self.kind {
KeyDown | SysKeyDown => KeyInteractionKind::Press,
KeyUp | SysKeyUp => KeyInteractionKind::Release,
_ => KeyInteractionKind::Other,
}
}
pub fn stroke(&self) -> Keystroke {
self.stroke
}
}
impl Display for KeyMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.interaction_kind(), self.stroke)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, derive_more::Display)]
pub enum KeyInteractionKind {
Press,
Release,
Other,
}
impl KeyInteractionKind {
pub fn is_press(&self) -> bool {
matches!(self, Self::Press)
}
/// Checks if the kind is relevant (only key-down and key-up).
pub fn is_press_or_release(&self) -> bool {
matches!(self, Self::Press | Self::Release)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
pub struct Keystroke {
modifiers: BitFlags<AcceleratorBehavior>,
key: AcceleratorKeyCode,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, derive_more::Display)]
pub enum KeyStrokePortability {
NonPortable(PortabilityIssue),
Portable,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, derive_more::Display)]
pub enum PortabilityIssue {
NotNormalized,
OperatingSystemRelated,
KeyboardLayoutRelated,
Other,
}
impl Keystroke {
pub fn new(behavior: BitFlags<AcceleratorBehavior>, key: AcceleratorKeyCode) -> Self {
Self {
modifiers: behavior,
key,
}
}
/// This normalizes the given behavior/key combination so it works cross-platform.
///
/// When REAPER notifies us about incoming key events, the accelerator behavior and key codes
/// look slightly different depending on the operating system:
///
/// - On all operating systems, if we have a key combination, we receive each key event
/// separately, even the modifier keys. Good!
/// - If we have a key combination (modifier key + normal key), Windows doesn't mention the
/// modifier keys in the accelerator behavior, but macOS and Linux do. We prefer the Windows
/// way because it makes more sense in this context. We receive modifier key-ups and key-downs
/// separately anyway.
/// - On Windows, umlauts are delivered as virtual keys, on macOS and Linux as character codes.
/// We prefer the macOS and Linux way.
/// - On Windows, "normal" special characters such as # and + are delivered as virtual keys.
/// On macOS and Linux, they are delivered as character codes.
/// - On Windows, "abnormal" special characters such as ^ or ` are delivered as virtual keys.
/// On macOS, they are also delivered as virtual keys but with a different code.
/// On Linux, they are delivered as character code.
/// We don't like any. Mark them as non-portable!
#[allow(clippy::if_same_then_else)]
pub fn normalized(&self) -> Self {
use AcceleratorBehavior::*;
let mut modifiers = self.modifiers;
let key = self.key;
// Remove modifier info (makes a difference on macOS and Linux only).
modifiers.remove(Shift | Control | Alt);
// Do some Windows-specific conversions.
#[cfg(windows)]
{
if modifiers.contains(VirtKey) {
// Key is a virtual key.
// On Windows, we need to convert virtual keys for umlauts or special characters to
// character codes so we match the behavior of macOS and Linux.
let character_code = unsafe {
winapi::um::winuser::MapVirtualKeyW(
key.get() as u32,
winapi::um::winuser::MAPVK_VK_TO_CHAR,
)
};
if character_code == 0 {
// Couldn't find corresponding character code.
Self::new(modifiers, key)
} else if character_code == key.get() as u32 {
// Character code is equal to virtual key code. In this case, macOS and Linux
// would also use the virtual key code (I hope), so we keep it.
Self::new(modifiers, key)
} else {
// We have a completely different character code. Use this one because
// macOS and Linux would also prefer the character code.
modifiers.remove(VirtKey);
Self::new(modifiers, AcceleratorKeyCode::new(character_code as u16))
}
} else {
// Key is a character code. Use as is.
Self::new(modifiers, key)
}
}
// On Linux and macOS, this is not necessary.
#[cfg(not(windows))]
{
Self::new(modifiers, key)
}
}
pub fn modifiers(&self) -> BitFlags<AcceleratorBehavior> {
self.modifiers
}
pub fn key_code(&self) -> AcceleratorKeyCode {
self.key
}
/// Returns information about portability of this keystroke across operating systems, keyboards,
/// layouts, if known.
pub fn portability(&self) -> Option<KeyStrokePortability> {
use KeyStrokePortability::*;
use PortabilityIssue::*;
let normalized = self.normalized();
if *self != normalized {
return Some(KeyStrokePortability::NonPortable(
PortabilityIssue::NotNormalized,
));
}
match self.accelerator_key() {
AcceleratorKey::Character(ch) => {
match ch {
// Consider non-ASCII characters generally as non-portable.
x if x > 0x7f => Some(NonPortable(KeyboardLayoutRelated)),
a => {
let a = a as u8;
match a {
// These ones are at least on the numpad. Numpad is layout-agnostic.
b'+' | b'-' | b'*' | b'/' => Some(Portable),
// These have special behavior on some keyboard layouts.
b'`' | b'^' => Some(NonPortable(KeyboardLayoutRelated)),
// Since most ASCII characters are transmitted as virtual keys, we
// can categorize all other ASCII characters as probably not portable.
_ => None,
}
}
}
}
AcceleratorKey::VirtKey(k) => {
use virt_keys::*;
match k {
// Special keys that either every keyboard has or everybody knows a keyboard
// might not have. Anyway, no cross-platform or keyboard-layout issues usually.
ESCAPE | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | INSERT
| NUMPAD0 | NUMPAD1 | NUMPAD2 | NUMPAD3 | NUMPAD4 | NUMPAD5 | NUMPAD6
| NUMPAD7 | NUMPAD8 | NUMPAD9 | SHIFT | CONTROL | MENU | SPACE | TAB | HOME
| END | PRIOR | NEXT | LEFT | UP | DOWN | RIGHT | RETURN | BACK | PAUSE
| CLEAR | DELETE | SNAPSHOT => Some(Portable),
CAPITAL => {
// CAPS LOCK doesn't fire on macOS.
Some(NonPortable(OperatingSystemRelated))
}
F12 => {
// F12 is known to be treated a bit differently at times.
Some(NonPortable(PortabilityIssue::Other))
}
// Characters
k => match u8::try_from(k.get()) {
Ok(b'A'..=b'Z' | b'0'..=b'9') => Some(Portable),
// Other basic characters don't qualify as explicitly portable.
_ => None,
},
}
}
}
}
pub fn accelerator_key(&self) -> AcceleratorKey {
AcceleratorKey::from_behavior_and_key_code(self.modifiers, self.key)
}
pub fn is_modifier_key(&self) -> bool {
use virt_keys::{CONTROL, MENU, SHIFT};
matches!(
self.accelerator_key(),
AcceleratorKey::VirtKey(CONTROL | MENU | SHIFT)
)
}
fn format_key_via_reaper(&self) -> ReaperString {
let accel = Accel {
f_virt: self.modifiers,
key: self.key,
cmd: 0,
};
Reaper::get().medium_reaper().kbd_format_key_name(accel)
}
}
impl Display for Keystroke {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let key = self.accelerator_key();
use virt_keys::{CONTROL, MENU, SHIFT};
const WIN: VirtKey = VirtKey::new(91);
use AcceleratorKey as K;
let label: Cow<str> = match key {
K::VirtKey(SHIFT) => "Shift".into(),
K::VirtKey(CONTROL) => "Ctrl/Cmd".into(),
K::VirtKey(MENU) => "Alt/Opt".into(),
K::VirtKey(WIN) => "Win/^".into(),
_ => self.format_key_via_reaper().into_string().into(),
};
f.write_str(label.as_ref())
}
}