From 53c668b355983479c97a8da753f95f65bf83d69d Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Tue, 28 Apr 2026 17:34:45 -0500 Subject: [PATCH] macos: re-present window on app re-launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closing the menubar window hides it; re-launching Lan Mouse.app via Finder, `open`, or the Dock should bring the window back — but it didn't, because the kAEReopenApplication Apple Event was silently dropped. NSApp's default 'aevt'/'rapp' handler funnels the event into applicationShouldHandleReopen:hasVisibleWindows:, and GtkApplication owns NSApp's delegate without implementing that selector. Register the existing status-item delegate as a direct handler for 'aevt'/'rapp' via NSAppleEventManager. setEventHandler: replaces NSApplication's default, so we receive the event in our code and re-present the window plus call activateIgnoringOtherApps:. Extract a present_window() helper so the menubar's "Open Lan Mouse" item and the new re-open handler share one code path. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/src/macos_status_item.rs | 69 ++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/lan-mouse-gtk/src/macos_status_item.rs b/lan-mouse-gtk/src/macos_status_item.rs index d8aed2e..ef1a345 100644 --- a/lan-mouse-gtk/src/macos_status_item.rs +++ b/lan-mouse-gtk/src/macos_status_item.rs @@ -2,7 +2,7 @@ use std::{ cell::RefCell, - ffi::{CStr, CString, c_char, c_double, c_void}, + ffi::{CStr, CString, c_char, c_double, c_uint, c_void}, sync::OnceLock, }; @@ -45,7 +45,10 @@ pub fn setup(app: &adw::Application, window: &Window) { let hold = app.hold(); let ns_app = msg_send_id(class(c"NSApplication"), sel(c"sharedApplication")); - assert!(!ns_app.is_null(), "NSApplication sharedApplication returned null"); + assert!( + !ns_app.is_null(), + "NSApplication sharedApplication returned null" + ); msg_send_bool_usize(ns_app, sel(c"setActivationPolicy:"), 1); let delegate = new_delegate(); @@ -56,7 +59,10 @@ pub fn setup(app: &adw::Application, window: &Window) { ]); let status_bar = msg_send_id(class(c"NSStatusBar"), sel(c"systemStatusBar")); - assert!(!status_bar.is_null(), "NSStatusBar systemStatusBar returned null"); + assert!( + !status_bar.is_null(), + "NSStatusBar systemStatusBar returned null" + ); let status_item = msg_send_id_f64(status_bar, sel(c"statusItemWithLength:"), -1.0); assert!(!status_item.is_null(), "statusItemWithLength returned null"); // Retain so the status item survives autorelease pool drain. @@ -72,6 +78,8 @@ pub fn setup(app: &adw::Application, window: &Window) { msg_send_void_id(item, sel(c"setTarget:"), delegate); } + install_reopen_handler(delegate); + log::debug!("macos_status_item ready at {:p}", status_item); item.replace(Some(StatusItem { @@ -194,12 +202,29 @@ fn delegate_class() -> Class { quit_lan_mouse as *const c_void, c"v@:@".as_ptr(), ); + // kAEReopenApplication handler — fires when the user re-launches + // the .app while it's already running (Finder, `open`, Dock). + class_addMethod( + class, + sel(c"handleReopenEvent:withReplyEvent:"), + handle_reopen_event as *const c_void, + c"v@:@@".as_ptr(), + ); objc_registerClassPair(class); class as usize }) as Class } extern "C" fn show_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { + present_window(); +} + +extern "C" fn handle_reopen_event(_this: Id, _cmd: Sel, _event: Id, _reply: Id) { + log::debug!("kAEReopenApplication received — presenting main window"); + present_window(); +} + +fn present_window() { STATUS_ITEM.with(|item| { let item = item.borrow(); let Some(item) = item.as_ref() else { @@ -216,6 +241,35 @@ extern "C" fn show_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { }); } +// Register the status-item delegate as the handler for the +// kAEReopenApplication Apple Event ('aevt'/'rapp'). NSApplication +// installs a default handler at -finishLaunching that just delegates to +// applicationShouldHandleReopen:hasVisibleWindows: — which is a no-op +// here because GApplication owns NSApp's delegate. Replacing it lets us +// re-present the window when the user double-clicks the .app while +// we're already running. +unsafe fn install_reopen_handler(delegate: Id) { + const K_CORE_EVENT_CLASS: c_uint = 0x6165_7674; // 'aevt' + const K_AE_REOPEN_APPLICATION: c_uint = 0x7261_7070; // 'rapp' + + let manager = msg_send_id( + class(c"NSAppleEventManager"), + sel(c"sharedAppleEventManager"), + ); + if manager.is_null() { + log::warn!("NSAppleEventManager unavailable; re-launch will not re-open window"); + return; + } + msg_send_void_id_sel_u32_u32( + manager, + sel(c"setEventHandler:andSelector:forEventClass:andEventID:"), + delegate, + sel(c"handleReopenEvent:withReplyEvent:"), + K_CORE_EVENT_CLASS, + K_AE_REOPEN_APPLICATION, + ); +} + extern "C" fn quit_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { STATUS_ITEM.with(|item| { if let Some(app) = item.borrow().as_ref().and_then(|item| item.app.upgrade()) { @@ -280,4 +334,13 @@ extern "C" { fn msg_send_void_id(receiver: Id, selector: Sel, value: Id); #[link_name = "objc_msgSend"] fn msg_send_bool_usize(receiver: Id, selector: Sel, value: usize) -> Bool; + #[link_name = "objc_msgSend"] + fn msg_send_void_id_sel_u32_u32( + receiver: Id, + selector: Sel, + a: Id, + b: Sel, + c: c_uint, + d: c_uint, + ); }