Skip to content

Commit

Permalink
#1083 Last step: Allow text entry on Windows when pressing global hotkey
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Aug 20, 2024
1 parent f60b73b commit 90a6494
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 53 deletions.
103 changes: 78 additions & 25 deletions main/src/infrastructure/plugin/backbone_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ static APP_LIBRARY: std::sync::OnceLock<anyhow::Result<crate::infrastructure::ui
std::sync::OnceLock::new();

pub type RealearnSessionAccelerator =
RealearnAccelerator<WeakUnitModel, BackboneHelgoboxWindowSnitch>;
RealearnAccelerator<WeakUnitModel, BackboneHelgoboxWindowSnitch>;

pub type RealearnControlSurface =
MiddlewareControlSurface<RealearnControlSurfaceMiddleware<WeakUnitModel>>;
MiddlewareControlSurface<RealearnControlSurfaceMiddleware<WeakUnitModel>>;

/// Just the old term as alias for easier class search.
type _App = BackboneShell;
Expand Down Expand Up @@ -659,7 +659,8 @@ impl BackboneShell {
session
.plugin_register_add_hook_post_command::<Self>()
.unwrap();
// Window hooks (fails before REAPER 6.29)
// Window hooks (fails before REAPER 6.29). Only necessary on Windows, see https://github.com/helgoboss/helgobox/issues/1083.
#[cfg(windows)]
let _ = session.plugin_register_add_hwnd_info::<Self>();
// This fails before REAPER 6.20 and therefore we don't have MIDI CC action feedback.
let _ =
Expand Down Expand Up @@ -920,7 +921,7 @@ impl BackboneShell {
#[allow(dead_code)]
pub fn spawn_in_async_runtime<R>(
&self,
f: impl Future<Output = R> + Send + 'static,
f: impl Future<Output=R> + Send + 'static,
) -> tokio::task::JoinHandle<R>
where
R: Send + 'static,
Expand Down Expand Up @@ -1047,7 +1048,7 @@ impl BackboneShell {
self.controller_preset_manager.borrow().log_debug_info();
}

pub fn changed(&self) -> impl LocalObservable<'static, Item = (), Err = ()> + 'static {
pub fn changed(&self) -> impl LocalObservable<'static, Item=(), Err=()> + 'static {
self.sessions_changed_subject.borrow().clone()
}

Expand Down Expand Up @@ -1227,7 +1228,7 @@ impl BackboneShell {
.iter()
.find(|i| i.is_main_unit && i.instance_id == instance_id)
})
.ok()
.ok()
}

#[cfg(feature = "playtime")]
Expand Down Expand Up @@ -1921,7 +1922,7 @@ impl BackboneShell {
compartment,
target,
)
.or_else(|| self.find_first_session_with_target(None, compartment, target))
.or_else(|| self.find_first_session_with_target(None, compartment, target))
}

fn find_first_session_with_target(
Expand Down Expand Up @@ -1976,7 +1977,7 @@ impl BackboneShell {
Some(Reaper::get().current_project()),
input_descriptor,
)
.or_else(|| self.find_first_session_with_input_from(None, input_descriptor))
.or_else(|| self.find_first_session_with_input_from(None, input_descriptor))
}

fn find_first_session_with_input_from(
Expand All @@ -2001,13 +2002,13 @@ impl BackboneShell {
compartment,
capture_result,
)
.or_else(|| {
self.find_first_session_with_learnable_source_matching(
None,
compartment,
capture_result,
)
})
.or_else(|| {
self.find_first_session_with_learnable_source_matching(
None,
compartment,
capture_result,
)
})
}

fn find_first_session_with_learnable_source_matching(
Expand Down Expand Up @@ -2256,17 +2257,69 @@ impl HookPostCommand for BackboneShell {

impl HwndInfo for BackboneShell {
fn call(window: Hwnd, info_type: HwndInfoType) -> i32 {
if info_type == HwndInfoType::IsTextField {
if app_window_is_in_text_entry_mode(window) {
println!("IN TEXT ENTRY MODE");
1
} else {
println!("NOT IN TEXT ENTRY MODE");
-1
match info_type {
HwndInfoType::IsTextField => {
// REAPER detected a global hotkey press while having a child window focused. It wants to know whether
// this child window is currently in text-entry mode, in which case it would NOT execute the action
// associated with the global hotkey but direct the key to the window. We must check here whether
// the Helgobox App is currently in text entry mode. This is only necessary on Windows, because
// Flutter essentially just uses one big HWND on windows ... text fields are not different HWNDs and
// therefore not identifiable as text field (via Window classes "Edit", "RichEdit" etc.).
// When we end up here, we are on Windows (for macOS, the hook is not registered). On Windows, the
// window associated with the app instance is always the parent window of the window for which REAPER
// queries the HwndInfo. So we need to check the parent window.
if let Some(parent_window) = Window::from_hwnd(window).parent() {
// The queried window has a parent
match app_window_is_in_text_entry_mode(parent_window.raw_hwnd()) {
None => {
// Probably not a Helgobox App window
0
}
Some(false) => {
// It's a Helgobox App window, but we are not in text entry mode
-1
}
Some(true) => {
// It's a Helgobox App, and we are in text entry mode
1
}
}
} else {
// The queried window doesn't have any parent. Then it can't be the app window.
0
}
}
HwndInfoType::ShouldProcessGlobalHotkeys | HwndInfoType::Unknown(_) => {
// This is called when the hotkey is defined with scope "Global + text fields". In this case,
// we don't need to do anything because we want the global hotkey to fire.
0
}
} else {
0
}
// let window = Window::from_hwnd(window);
// // TODO-high CONTINUE Better cache HWNDs in a set
// BackboneShell::get().with_instance_shell_infos(|infos| {
// for i in infos {
// if let Some(i) = i.instance_shell.upgrade() {
// let app_instance = i.panel().app_instance();
// let app_instance_window = app_instance.borrow().window();
// println!("queried {window:?} vs. actual {app_instance_window:?}");
// if let Some(w) = app_instance_window {
// if w == window.raw_hwnd() {
// println!("Matched window directly!");
// return 1;
// }
// if let Some(parent_window) = window.parent() {
// if w == parent_window.raw_hwnd() {
// println!("Matched parent window!");
// return 1;
// }
// }
// }
// }
// }
// // No app window matches
// 0
// })
}
}

Expand Down Expand Up @@ -2615,7 +2668,7 @@ fn decompress_app() -> anyhow::Result<()> {
let mut archive = tar::Archive::new(tar);
if destination_dir.exists() {
#[cfg(target_family = "windows")]
let context = "Couldn't clean up existing app directory. This can happen if you have \"Allow complete unload of VST plug-ins\" enabled in REAPER preferences => Plug-ins => VST. Turn this option off and restart REAPER before using the app.";
let context = "Couldn't clean up existing app directory. This can happen if you have \"Allow complete unload of VST plug-ins\" enabled in REAPER preferences => Plug-ins => VST. Turn this option off and restart REAPER before using the app.";
#[cfg(target_family = "unix")]
let context = "Couldn't remove existing app directory";
fs::remove_dir_all(destination_dir).context(context)?;
Expand Down
39 changes: 27 additions & 12 deletions main/src/infrastructure/ui/app/app_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::infrastructure::proto::{
};
use crate::infrastructure::ui::AppHandle;
use anyhow::{anyhow, bail, Context, Result};
use base::hash_util::NonCryptoHashSet;
use base::hash_util::{NonCryptoHashMap, NonCryptoHashSet};
use fragile::Fragile;
use once_cell::sync::Lazy;
use prost::Message;
Expand Down Expand Up @@ -36,6 +36,8 @@ pub trait AppInstance: Debug {

fn notify_app_is_ready(&mut self, callback: AppCallback);

fn window(&self) -> Option<Hwnd>;

fn notify_app_is_in_text_entry_mode(&mut self, is_in_text_entry_mode: bool);
}

Expand Down Expand Up @@ -127,6 +129,10 @@ impl AppInstance for DummyAppInstance {

fn notify_app_is_ready(&mut self, _callback: AppCallback) {}

fn window(&self) -> Option<Hwnd> {
None
}

fn notify_app_is_in_text_entry_mode(&mut self, _is_in_text_entry_mode: bool) {}
}

Expand Down Expand Up @@ -231,6 +237,7 @@ impl AppInstance for StandaloneAppInstance {
.send(reply)
}


fn notify_app_is_ready(&mut self, callback: AppCallback) {
let Some(running_state) = &mut self.running_state else {
return;
Expand All @@ -253,23 +260,25 @@ impl AppInstance for StandaloneAppInstance {
running_state.event_subscription_join_handle = Some(join_handle);
}

fn window(&self) -> Option<Hwnd> {
let running_state = self.running_state.as_ref()?;
running_state.common_state.window()
}

fn notify_app_is_in_text_entry_mode(&mut self, is_in_text_entry_mode: bool) {
let mut set = APP_WINDOWS_IN_TEXT_ENTRY.get();
return;
let hwnd = todo!();
if is_in_text_entry_mode {
set.insert(hwnd);
} else {
set.remove(&hwnd);
}
let mut map = APP_WINDOWS_IN_TEXT_ENTRY.get().borrow_mut();
let Some(hwnd) = self.window() else {
return;
};
map.insert(hwnd, is_in_text_entry_mode);
}
}

static APP_WINDOWS_IN_TEXT_ENTRY: Lazy<Fragile<NonCryptoHashSet<Hwnd>>> =
static APP_WINDOWS_IN_TEXT_ENTRY: Lazy<Fragile<RefCell<NonCryptoHashMap<Hwnd, bool>>>> =
Lazy::new(|| Default::default());

pub fn app_window_is_in_text_entry_mode(window: Hwnd) -> bool {
APP_WINDOWS_IN_TEXT_ENTRY.get().contains(&window)
pub fn app_window_is_in_text_entry_mode(window: Hwnd) -> Option<bool> {
APP_WINDOWS_IN_TEXT_ENTRY.get().borrow().get(&window).copied()
}

#[derive(Debug)]
Expand All @@ -285,6 +294,12 @@ impl CommonAppRunningState {
Ok(())
}

pub fn window(&self) -> Option<Hwnd> {
let app_library = BackboneShell::get_app_library().ok()?;
app_library
.app_instance_get_window(self.app_handle).ok().flatten()
}

pub fn is_visible(&self) -> bool {
let Ok(app_library) = BackboneShell::get_app_library() else {
return false;
Expand Down
33 changes: 24 additions & 9 deletions main/src/infrastructure/ui/app/app_library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::ffi::{c_char, c_uint, c_void, CStr, CString};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::ptr::{null_mut, NonNull};
use reaper_medium::Hwnd;
use swell_ui::Window;
use tonic::Status;

Expand Down Expand Up @@ -49,7 +50,7 @@ impl AppLibrary {
"window_manager_plugin.dll",
"pointer_lock_plugin.dll",
]
.as_slice(),
.as_slice(),
)
} else if cfg!(target_os = "macos") {
(
Expand Down Expand Up @@ -190,6 +191,17 @@ impl AppLibrary {
Ok(visible)
}

pub fn app_instance_get_window(&self, app_handle: AppHandle) -> Result<Option<Hwnd>> {
let hwnd = unsafe {
let get_app_instance_window: Symbol<GetAppInstanceWindow> = self
.main_library
.get(b"get_app_instance_window\0")
.map_err(|_| anyhow!("failed to load get_app_instance_window function"))?;
get_app_instance_window(app_handle)
};
Ok(Hwnd::new(hwnd))
}

pub fn app_instance_has_focus(&self, app_handle: AppHandle) -> Result<bool> {
let visible = unsafe {
let app_instance_has_focus: Symbol<AppInstanceHasFocus> = self
Expand Down Expand Up @@ -317,6 +329,9 @@ type AppInstanceHasFocus = unsafe extern "C" fn(app_handle: AppHandle) -> bool;
/// Signature of the function that we use to check whether an app instance is visible.
type AppInstanceIsVisible = unsafe extern "C" fn(app_handle: AppHandle) -> bool;

/// Signature of the function that we use to acquire the app window.
type GetAppInstanceWindow = unsafe extern "C" fn(app_handle: AppHandle) -> HWND;

/// Signature of the function that we use to stop an app instance.
type StopAppInstance = unsafe extern "C" fn(parent_window: HWND, app_handle: AppHandle);

Expand Down Expand Up @@ -489,7 +504,7 @@ fn process_command(
.unwrap();
create_initial_instance_updates(&instance_shell)
})
.map_err(to_status)?;
.map_err(to_status)?;
}
GetOccasionalUnitUpdates(req) => {
send_initial_events_to_app(instance_id, || {
Expand All @@ -498,7 +513,7 @@ fn process_command(
.unwrap();
create_initial_unit_updates(&instance_shell)
})
.map_err(to_status)?;
.map_err(to_status)?;
}
GetOccasionalPlaytimeEngineUpdates(_) => {
#[cfg(not(feature = "playtime"))]
Expand All @@ -511,7 +526,7 @@ fn process_command(
instance_id,
crate::infrastructure::proto::create_initial_engine_updates,
)
.map_err(to_status)?;
.map_err(to_status)?;
}
}
GetOccasionalMatrixUpdates(req) => {
Expand All @@ -527,7 +542,7 @@ fn process_command(
req.matrix_id.into(),
proto::create_initial_matrix_updates,
)
.map_err(to_status)?;
.map_err(to_status)?;
}
}
GetOccasionalTrackUpdates(req) => {
Expand All @@ -543,7 +558,7 @@ fn process_command(
req.matrix_id.into(),
proto::create_initial_track_updates,
)
.map_err(to_status)?;
.map_err(to_status)?;
}
}
GetOccasionalSlotUpdates(req) => {
Expand All @@ -559,7 +574,7 @@ fn process_command(
req.matrix_id.into(),
proto::create_initial_slot_updates,
)
.map_err(to_status)?;
.map_err(to_status)?;
}
}
GetOccasionalClipUpdates(req) => {
Expand All @@ -575,7 +590,7 @@ fn process_command(
req.matrix_id.into(),
proto::create_initial_clip_updates,
)
.map_err(to_status)?;
.map_err(to_status)?;
}
}
// Normal commands
Expand Down Expand Up @@ -746,7 +761,7 @@ fn send_event_reply_to_app(instance_id: InstanceId, value: event_reply::Value) -
fn send_query_reply_to_app(
instance_id: InstanceId,
req_id: u32,
future: impl Future<Output = Result<query_result::Value, Status>> + Send + 'static,
future: impl Future<Output=Result<query_result::Value, Status>> + Send + 'static,
) {
Global::future_support().spawn_in_main_thread(async move {
let query_result_value = match future.await {
Expand Down
Loading

0 comments on commit 90a6494

Please sign in to comment.