From 085ae2a27e367d355a5801d19744aeed99bbe61f Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 25 Mar 2024 18:34:10 +0200 Subject: [PATCH] add support for Window::Focused/Unfocused events on macOS (#171) trigger `WindowEvent::Focused` and `WindowEvent::Unfocused` events when the plugin window gains/loses focus. implemented by adding observers to `NSNotificationCenter::defaultCenter()` that listen to `NSWindowDidBecomeKeyNotification` and `NSWindowDidResignKeyNotification` notifications on the `NSViews`' window. tested and confirmed to work in Live, Bitwig, FL Studio, Reaper and AudioPluginHost. --- src/macos/view.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/macos/window.rs | 8 +++-- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/macos/view.rs b/src/macos/view.rs index 66dd57a..8b765b4 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -29,6 +29,12 @@ use super::{ /// Name of the field used to store the `WindowState` pointer. pub(super) const BASEVIEW_STATE_IVAR: &str = "baseview_state"; +#[link(name = "AppKit", kind = "framework")] +extern "C" { + static NSWindowDidBecomeKeyNotification: id; + static NSWindowDidResignKeyNotification: id; +} + macro_rules! add_simple_mouse_class_method { ($class:ident, $sel:ident, $event:expr) => { #[allow(non_snake_case)] @@ -94,6 +100,18 @@ macro_rules! add_simple_keyboard_class_method { }; } +unsafe fn register_notification(observer: id, notification_name: id, object: id) { + let notification_center: id = msg_send![class!(NSNotificationCenter), defaultCenter]; + + let _: () = msg_send![ + notification_center, + addObserver:observer + selector:sel!(handleNotification:) + name:notification_name + object:object + ]; +} + pub(super) unsafe fn create_view(window_options: &WindowOpenOptions) -> id { let class = create_view_class(); @@ -103,6 +121,9 @@ pub(super) unsafe fn create_view(window_options: &WindowOpenOptions) -> id { view.initWithFrame_(NSRect::new(NSPoint::new(0., 0.), NSSize::new(size.width, size.height))); + register_notification(view, NSWindowDidBecomeKeyNotification, nil); + register_notification(view, NSWindowDidResignKeyNotification, nil); + let _: id = msg_send![ view, registerForDraggedTypes: NSArray::arrayWithObjects(nil, &[NSFilenamesPboardType]) @@ -124,6 +145,14 @@ unsafe fn create_view_class() -> &'static Class { sel!(acceptsFirstResponder), property_yes as extern "C" fn(&Object, Sel) -> BOOL, ); + class.add_method( + sel!(becomeFirstResponder), + become_first_responder as extern "C" fn(&Object, Sel) -> BOOL, + ); + class.add_method( + sel!(resignFirstResponder), + resign_first_responder as extern "C" fn(&Object, Sel) -> BOOL, + ); class.add_method(sel!(isFlipped), property_yes as extern "C" fn(&Object, Sel) -> BOOL); class.add_method( sel!(preservesContentInLiveResize), @@ -177,6 +206,10 @@ unsafe fn create_view_class() -> &'static Class { dragging_updated as extern "C" fn(&Object, Sel, id) -> NSUInteger, ); class.add_method(sel!(draggingExited:), dragging_exited as extern "C" fn(&Object, Sel, id)); + class.add_method( + sel!(handleNotification:), + handle_notification as extern "C" fn(&Object, Sel, id), + ); add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left); add_mouse_button_class_method!(class, mouseUp, ButtonReleased, MouseButton::Left); @@ -208,6 +241,25 @@ extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL YES } +extern "C" fn become_first_responder(this: &Object, _sel: Sel) -> BOOL { + let state = unsafe { WindowState::from_view(this) }; + let is_key_window = unsafe { + let window: id = msg_send![this, window]; + let is_key_window: BOOL = msg_send![window, isKeyWindow]; + is_key_window == YES + }; + if is_key_window { + state.trigger_event(Event::Window(WindowEvent::Focused)); + } + YES +} + +extern "C" fn resign_first_responder(this: &Object, _sel: Sel) -> BOOL { + let state = unsafe { WindowState::from_view(this) }; + state.trigger_event(Event::Window(WindowEvent::Unfocused)); + YES +} + extern "C" fn window_should_close(this: &Object, _: Sel, _sender: id) -> BOOL { let state = unsafe { WindowState::from_view(this) }; @@ -473,3 +525,30 @@ extern "C" fn dragging_exited(this: &Object, _sel: Sel, _sender: id) { on_event(&state, MouseEvent::DragLeft); } + +extern "C" fn handle_notification(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let state = WindowState::from_view(this); + + // The subject of the notication, in this case an NSWindow object. + let notification_object: id = msg_send![notification, object]; + + // The NSWindow object associated with our NSView. + let window: id = msg_send![this, window]; + + let first_responder: id = msg_send![window, firstResponder]; + + // Only trigger focus events if the NSWindow that's being notified about is our window, + // and if the window's first responder is our NSView. + // If the first responder isn't our NSView, the focus events will instead be triggered + // by the becomeFirstResponder and resignFirstResponder methods on the NSView itself. + if notification_object == window && first_responder == this as *const Object as id { + let is_key_window: BOOL = msg_send![window, isKeyWindow]; + state.trigger_event(Event::Window(if is_key_window == YES { + WindowEvent::Focused + } else { + WindowEvent::Unfocused + })); + } + } +} diff --git a/src/macos/window.rs b/src/macos/window.rs index c041252..ea8b78f 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -13,9 +13,8 @@ use core_foundation::runloop::{ CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, __CFRunLoopTimer, kCFRunLoopDefaultMode, }; use keyboard_types::KeyboardEvent; - +use objc::class; use objc::{msg_send, runtime::Object, sel, sel_impl}; - use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, @@ -83,6 +82,11 @@ impl WindowInner { CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode); } + // Deregister NSView from NotificationCenter. + let notification_center: id = + msg_send![class!(NSNotificationCenter), defaultCenter]; + let () = msg_send![notification_center, removeObserver:self.ns_view]; + drop(window_state); // Close the window if in non-parented mode