From 72287817087122817fd9511330ed75b49f7827c8 Mon Sep 17 00:00:00 2001 From: Ying Zhong <0x00eeee@gmail.com> Date: Mon, 19 Oct 2026 16:01:38 +0800 Subject: [PATCH] [macOS 27] Use native menu presentation --- TextGrabber2/Sources/App/App+Event.swift | 6 +++ TextGrabber2/Sources/App/App+Menu.swift | 47 +++++++++++++++++++++++- TextGrabber2/Sources/App/App.swift | 4 +- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/TextGrabber2/Sources/App/App+Event.swift b/TextGrabber2/Sources/App/App+Event.swift index fdb4f93..3c28fe9 100644 --- a/TextGrabber2/Sources/App/App+Event.swift +++ b/TextGrabber2/Sources/App/App+Event.swift @@ -43,6 +43,12 @@ extension App { return event } + if self?.usesNativeMenuPresentation == true, + self?.isMenuVisible == true, + self?.statusItem.menu != nil { + return event + } + if let window = NSApp.popoverWindow { window.closePopover() } else { diff --git a/TextGrabber2/Sources/App/App+Menu.swift b/TextGrabber2/Sources/App/App+Menu.swift index c1ad8fc..505cd8a 100644 --- a/TextGrabber2/Sources/App/App+Menu.swift +++ b/TextGrabber2/Sources/App/App+Menu.swift @@ -10,7 +10,25 @@ import AppKit // MARK: - Menu extension App { + var usesNativeMenuPresentation: Bool { + // Native presentation is needed for the status item highlight on macOS 27 + if #available(macOS 27.0, *) { + return true + } + + return false + } + func statusItemClicked() { + guard !isMenuVisible else { + return + } + + if usesNativeMenuPresentation { + // A new request may arrive before the previous menu's deferred cleanup + statusItem.menu = nil + } + // Hide the user guide after the user becomes familiar if userClickCount > 4 && !NSPasteboard.general.hasLimitedAccess { howToItem.isHidden = true @@ -31,11 +49,21 @@ extension App { } func presentMainMenu() { + guard let button = statusItem.button else { + isMenuVisible = false + return Logger.log(.error, "Missing status item button") + } + let appearance = NSApp.effectiveAppearance mainMenu.appearance = appearance - let location = CGPoint(x: -8, y: (statusItem.button?.frame.height ?? 0) + 4) - mainMenu.popUp(positioning: nil, at: location, in: statusItem.button) + if usesNativeMenuPresentation { + statusItem.menu = mainMenu + button.performClick(nil) + } else { + let location = CGPoint(x: -8, y: button.frame.height + 4) + mainMenu.popUp(positioning: nil, at: location, in: button) + } } func showAppUpdate(name: String, url: String) { @@ -73,6 +101,10 @@ extension App: NSMenuDelegate { } func menuWillOpen(_ menu: NSMenu) { + if usesNativeMenuPresentation, menu == mainMenu { + isMenuVisible = true + } + guard menu == servicesItem.submenu else { return } @@ -83,6 +115,17 @@ extension App: NSMenuDelegate { func menuDidClose(_ menu: NSMenu) { if menu == mainMenu { + if usesNativeMenuPresentation { + // Let AppKit finish native highlight cleanup before detaching the menu + DispatchQueue.main.async { [weak self] in + guard let self, !self.isMenuVisible else { + return + } + + self.statusItem.menu = nil + } + } + isMenuVisible = false } diff --git a/TextGrabber2/Sources/App/App.swift b/TextGrabber2/Sources/App/App.swift index 484a93f..cd5506a 100644 --- a/TextGrabber2/Sources/App/App.swift +++ b/TextGrabber2/Sources/App/App.swift @@ -313,7 +313,9 @@ final class App: NSObject, NSApplicationDelegate { registerKeyBindings() } - statusItem.button?.highlight(isMenuVisible) + if !usesNativeMenuPresentation { + statusItem.button?.highlight(isMenuVisible) + } } }