diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9e02a874..ca91034e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -174,6 +174,8 @@ pub fn run() { app.manage(windows); app.manage(application_focus::ApplicationFocus::default()); #[cfg(target_os = "macos")] + macos::intercept_system_quit(app.handle()); + #[cfg(target_os = "macos")] application_focus::observe(app.handle()); app.manage(menu_state); if let Some(paths) = setup_opens.lock().take() { @@ -254,7 +256,11 @@ fn window_state_flags() -> tauri_plugin_window_state::StateFlags { /// Routes IDE menu items to the focused window's frontend (`executeAction`). fn on_menu_event(app: &tauri::AppHandle, event: tauri::menu::MenuEvent) { - let id = event.id().as_ref(); + dispatch_menu_action(app, event.id().as_ref()); +} + +/// Runs menu item `id` as if it were chosen: in the focused window's frontend, or here without one. +pub(crate) fn dispatch_menu_action(app: &tauri::AppHandle, id: &str) { let Some(route) = menu::route(&menu::spec(menu::Os::current()), id) else { return }; match windows::target_window(app) { Some(w) => { diff --git a/src-tauri/src/macos.rs b/src-tauri/src/macos.rs index fd1cd74b..1a5ce3ca 100644 --- a/src-tauri/src/macos.rs +++ b/src-tauri/src/macos.rs @@ -202,6 +202,43 @@ fn present(fullscreen: bool) { NSApplication::sharedApplication(mtm).setPresentationOptions(options); } +type ShouldTerminate = unsafe extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject) -> usize; + +/// `NSApplicationTerminateReply`. +const TERMINATE_CANCEL: usize = 0; +const TERMINATE_NOW: usize = 1; + +/// The Dock's Quit, the system's app menu and a logout ask the application delegate whether it +/// may terminate; tao's delegate doesn't answer, so AppKit terminated at once, past the exit, +/// running-process and unsaved-changes prompts. Answer it: cancel, and quit like Cmd+Q does. +pub fn intercept_system_quit(app: &AppHandle) { + let _ = APP.set(app.clone()); + let Some(mtm) = MainThreadMarker::new() else { return }; + let application = objc2_app_kit::NSApplication::sharedApplication(mtm); + unsafe { + let delegate: *mut AnyObject = objc2::msg_send![&*application, delegate]; + if delegate.is_null() { + return; + } + let class = objc2::ffi::object_getClass(delegate.cast()) as *mut AnyClass; + let imp: Imp = std::mem::transmute(should_terminate as ShouldTerminate); + let added = objc2::ffi::class_addMethod(class, objc2::sel!(applicationShouldTerminate:), imp, c"Q@:@".as_ptr()); + if !added.as_bool() { + tracing::warn!("could not handle applicationShouldTerminate:"); + } + } +} + +unsafe extern "C-unwind" fn should_terminate(_this: *mut AnyObject, _selector: Sel, _sender: *mut AnyObject) -> usize { + let Some(app) = APP.get() else { return TERMINATE_NOW }; + // Our own quit exits through the event loop; a system quit during it goes ahead. + if app.try_state::().is_none_or(|w| w.quitting()) { + return TERMINATE_NOW; + } + crate::dispatch_menu_action(app, "app.quit"); + TERMINATE_CANCEL +} + type ToggleFullScreen = unsafe extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject); static NATIVE_TOGGLE_FULLSCREEN: std::sync::OnceLock = std::sync::OnceLock::new();