From 70a1cc2e63d2c49a135bd89cec3ffb1d427d90ee Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 30 Jun 2026 16:21:47 +0800
Subject: [PATCH] Freeze prep TagLauncher 8.2.9 Pro status UI

---
 src/Apptag/ApptagApp.swift |  524 +++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 480 insertions(+), 44 deletions(-)

diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index e155bc6..0166772 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -17,7 +17,7 @@
         Settings {
             PreferencesView()
         }
-        .defaultSize(width: 1000, height: 480)
+        .defaultSize(width: 1000, height: 640)
         .commands {
             CommandGroup(replacing: .systemServices) { }
             CommandGroup(replacing: .appVisibility) { }
@@ -28,29 +28,44 @@
 
 // MARK: - App Delegate (menubar + overlay window + hotkey)
 
+private enum HotkeyRegistrationAttempt {
+    case success(EventHotKeyRef)
+    case failure(OSStatus)
+}
+
 final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
+    static private(set) weak var shared: AppDelegate?
+
     private static let showDockIconKey = "showDockIcon"
     private static let statusItemAutosaveName = AppIdentity.statusItemAutosaveName
     private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton")
     private static let statusItemAccessibilityLabel = AppIdentity.displayName
     private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem")
+    private static let proStatusMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherProStatusMenuItem")
     private static let helpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherHelpMenu")
     private static let downloadHelpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherDownloadHelpMenuItem")
     private static let externalActivationNotification = Notification.Name("TagLauncherExternalActivationRequested")
     private static let externalActivationObject = AppIdentity.bundleIdentifier
+    private static let externalInvocationScheme = "taglauncher"
+    private static let externalInvocationShowHost = "show"
     private static let duplicateLaunchSuppressReopenKey = "duplicateLaunchSuppressReopenAt"
+    private static let overlaySpaceHotkeyID: UInt32 = 3
     private static let launcherOverlayLevel = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue - 1)
     private static let overlayDefaultLevel = launcherOverlayLevel
     private static let overlayTextInputLevel = launcherOverlayLevel
+    private static let settingsContentSize = NSSize(width: 1000, height: 640)
 
     private var statusItem: NSStatusItem?
     private var overlayKeyMonitor: Any?
     private var quickSearchLocalMouseMonitor: Any?
     private var quickSearchExternalMouseMonitor: Any?
     private var settingsWindow: NSWindow?    // Track Settings window to keep it above overlay
+    private var explicitPreferencesOpenRequestedAt = Date.distantPast
     private var mainHotkeyRef: EventHotKeyRef?
     private var quickSearchHotkeyRef: EventHotKeyRef?
+    private var overlaySpaceHotkeyRef: EventHotKeyRef?
     private var hotkeyEventHandlerInstalled = false
+    private var configuredHotkeysSuspendedForRecording = false
     private var isQuickSearchOpen = false
     private var quickSearchShouldHideOverlayOnClose = false
     private var quickSearchOnlyOverlaySession = false
@@ -63,6 +78,13 @@
     private var statusMenuScreenForNextOverlay: NSScreen?
     private var suppressReopenUntil = Date.distantPast
     private var overlayOpenedByQuickSearchOnly = false
+    private var didFinishLaunching = false
+    private var pendingShowOverlayInvocation = false
+
+    override init() {
+        super.init()
+        Self.shared = self
+    }
 
     private lazy var overlayController = OverlayWindowController(
         dependencies: OverlayWindowController.Dependencies(
@@ -125,14 +147,16 @@
             refreshChromeState: { [weak self] activate, avoidSpaceSwitch in
                 self?.refreshLauncherChromeState(activate: activate, avoidSpaceSwitch: avoidSpaceSwitch)
             },
-            onWillHide: {
+            onWillHide: { [weak self] in
+                self?.unregisterOverlaySpaceHotkey()
                 TagDatabase.flushPendingCategorySchemeBackupBatch()
             },
             onDidHide: { [weak self] in
                 self?.overlayOpenedByQuickSearchOnly = false
                 NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
             },
-            onDidShow: {
+            onDidShow: { [weak self] in
+                self?.refreshOverlaySpaceHotkeyRegistration()
                 NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
             }
         )
@@ -192,8 +216,10 @@
     func applicationDidFinishLaunching(_ notification: Notification) {
         AppDefaults.register()
         L10n.setup()
+        ProEntitlementCenter.shared.start()
         migrateDefaultGroupName()
         TagDatabase.seedDefaultTags()
+        _ = TagDatabase.relocalizeSystemTagsForCurrentLanguage()
         syncChromeSettings(force: true)
         observeHotkeyStatusChanges()
         registerConfiguredHotkeys()
@@ -207,8 +233,12 @@
         observeApplicationMenuChanges()
         observeChromeSettings()
         observeLanguageChanges()
+        observeProEntitlementChanges()
         setupLaunchAtLogin()
         suppressReopenUntil = Date().addingTimeInterval(1.0)
+        didFinishLaunching = true
+        closeRestoredPreferencesWindowsDuringLaunch()
+        consumePendingShowOverlayInvocationIfNeeded()
         configureApplicationMenuWhenAvailable(retries: 200)
         warmAppIndexInBackground()
         relocalizeDefaultAppNotesForCurrentLanguageAsync()
@@ -241,6 +271,54 @@
         return false  // Suppress default "unhide all windows" behavior
     }
 
+    func application(_ application: NSApplication, open urls: [URL]) {
+        for url in urls {
+            handleExternalInvocationURL(url)
+        }
+    }
+
+    private enum ExternalInvocationRoute {
+        case showOverlay
+    }
+
+    private func handleExternalInvocationURL(_ url: URL) {
+        guard externalInvocationRoute(for: url) == .showOverlay else {
+            Diagnostics.log("app.externalInvocation.ignored", [
+                "url": url.absoluteString
+            ])
+            return
+        }
+        requestShowOverlayFromExternalInvocation()
+    }
+
+    private func externalInvocationRoute(for url: URL) -> ExternalInvocationRoute? {
+        guard url.scheme?.lowercased() == Self.externalInvocationScheme else {
+            return nil
+        }
+        let host = url.host?.lowercased()
+        let path = url.path
+        if host == Self.externalInvocationShowHost, path.isEmpty || path == "/" {
+            return .showOverlay
+        }
+        return nil
+    }
+
+    private func requestShowOverlayFromExternalInvocation() {
+        guard didFinishLaunching else {
+            pendingShowOverlayInvocation = true
+            return
+        }
+        pendingShowOverlayInvocation = false
+        suppressReopenUntil = Date().addingTimeInterval(1.0)
+        dismissQuickSearchIfNeeded()
+        showOrFocusOverlay()
+    }
+
+    private func consumePendingShowOverlayInvocationIfNeeded() {
+        guard pendingShowOverlayInvocation else { return }
+        requestShowOverlayFromExternalInvocation()
+    }
+
     private func isRecentUserClickNearDockArea() -> Bool {
         let lastMouseDown = CGEventSource.secondsSinceLastEventType(
             .combinedSessionState,
@@ -250,12 +328,15 @@
             .combinedSessionState,
             eventType: .leftMouseUp
         )
-        let recentMouseClick = min(lastMouseDown, lastMouseUp) < 1.2
+        let recentMouseClick = min(lastMouseDown, lastMouseUp) < 0.9
         return recentMouseClick && isPointerNearDockArea()
     }
 
     private func isPointerNearDockArea() -> Bool {
         let mouse = NSEvent.mouseLocation
+        let dockOrientation = UserDefaults(suiteName: "com.apple.dock")?
+            .string(forKey: "orientation") ?? "bottom"
+        let hiddenDockEdgeTolerance: CGFloat = 96
         return NSScreen.screens.contains { screen in
             let frame = screen.frame
             let visible = screen.visibleFrame
@@ -269,7 +350,16 @@
             let rightDock = visible.maxX < frame.maxX
                 && mouse.x <= frame.maxX
                 && mouse.x >= visible.maxX - 24
-            return bottomDock || leftDock || rightDock
+            let hiddenDockFallback: Bool
+            switch dockOrientation {
+            case "left":
+                hiddenDockFallback = mouse.x <= frame.minX + hiddenDockEdgeTolerance
+            case "right":
+                hiddenDockFallback = mouse.x >= frame.maxX - hiddenDockEdgeTolerance
+            default:
+                hiddenDockFallback = mouse.y <= frame.minY + hiddenDockEdgeTolerance
+            }
+            return bottomDock || leftDock || rightDock || hiddenDockFallback
         }
     }
 
@@ -277,6 +367,7 @@
         hideOverlay(force: true, discardWindow: true)
         unregisterHotkey(for: .main)
         unregisterHotkey(for: .quickSearch)
+        unregisterOverlaySpaceHotkey()
         TagDatabase.flushPendingCategorySchemeBackupBatch()
         removeOverlayKeyMonitor()
         removeQuickSearchExternalMouseMonitor()
@@ -329,11 +420,53 @@
         }
     }
 
+    private func foregroundWindowForActivationPolicyRestore() -> NSWindow? {
+        if let settingsWindow, settingsWindow.isVisible {
+            return settingsWindow
+        }
+        if let overlayWindow, overlayWindow.isVisible {
+            return overlayWindow
+        }
+        return NSApp.keyWindow
+    }
+
+    private func setLauncherActivationPolicy(_ desiredPolicy: NSApplication.ActivationPolicy) {
+        guard NSApp.activationPolicy() != desiredPolicy else { return }
+
+        let restoreWindow = desiredPolicy == .accessory && NSApp.isActive
+            ? foregroundWindowForActivationPolicyRestore()
+            : nil
+
+        // Deactivate before switching regular -> accessory; otherwise Dock can
+        // keep a stale running tile until Dock itself restarts.
+        if desiredPolicy == .accessory && NSApp.isActive {
+            NSApp.deactivate()
+        }
+
+        NSApp.setActivationPolicy(desiredPolicy)
+
+        guard desiredPolicy == .accessory,
+              let restoreWindow,
+              restoreWindow.isVisible
+        else { return }
+
+        DispatchQueue.main.async { [weak self, weak restoreWindow] in
+            guard let self,
+                  let restoreWindow,
+                  restoreWindow.isVisible,
+                  self.requiresForegroundOwnership
+            else { return }
+            NSApp.activate(ignoringOtherApps: true)
+            restoreWindow.makeKeyAndOrderFront(nil)
+            restoreWindow.orderFrontRegardless()
+        }
+    }
+
     private func beginLauncherForegroundOwnership(activate: Bool = true, keyWindow: NSWindow? = nil) {
         let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
         let desiredPolicy: NSApplication.ActivationPolicy = showDock ? .regular : .accessory
         if NSApp.activationPolicy() != desiredPolicy {
-            NSApp.setActivationPolicy(desiredPolicy)
+            setLauncherActivationPolicy(desiredPolicy)
         }
         if activate, showDock {
             claimLauncherForeground(keyWindow: keyWindow)
@@ -350,7 +483,7 @@
         overlayGeneration expectedOverlayGeneration: Int? = nil
     ) {
         if NSApp.activationPolicy() != .regular {
-            NSApp.setActivationPolicy(.regular)
+            setLauncherActivationPolicy(.regular)
         }
 
         if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) {
@@ -412,7 +545,7 @@
             ? .regular
             : (showDock ? .regular : .accessory))))
         if NSApp.activationPolicy() != desiredPolicy {
-            NSApp.setActivationPolicy(desiredPolicy)
+            setLauncherActivationPolicy(desiredPolicy)
         }
 
         let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : []
@@ -422,14 +555,17 @@
 
         if activate && requiresForegroundOwnership
             && !shouldStayAccessoryForCurrentFullscreenSpace
-            && !shouldStayAccessoryForHiddenDockChrome
             && !shouldStayAccessoryForQuickOnlySearch {
             let keyWindow = isSettingsVisible ? settingsWindow : (isOverlayVisible ? overlayWindow : nil)
-            claimLauncherForeground(
-                keyWindow: keyWindow,
-                retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
-                overlayGeneration: isOverlayVisible ? overlayGeneration : nil
-            )
+            if shouldStayAccessoryForHiddenDockChrome {
+                beginLauncherForegroundOwnership(activate: true, keyWindow: keyWindow)
+            } else {
+                claimLauncherForeground(
+                    keyWindow: keyWindow,
+                    retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
+                    overlayGeneration: isOverlayVisible ? overlayGeneration : nil
+                )
+            }
         }
     }
 
@@ -604,6 +740,9 @@
         versionItem.isEnabled = false
         menu.addItem(versionItem)
 
+        let proStatusItem = makeProStatusMenuItem()
+        menu.addItem(proStatusItem)
+
         menu.addItem(.separator())
         let prefsItem = NSMenuItem(
             title: tr("menu.preferences"),
@@ -640,6 +779,7 @@
     }
 
     func menuWillOpen(_ menu: NSMenu) {
+        configureProStatusMenuItem(in: menu)
         statusMenuScreenForNextOverlay = overlayController.screenContainingCurrentPointer()
             ?? statusItem?.button?.window?.screen
             ?? NSScreen.main
@@ -712,11 +852,58 @@
         return image
     }
 
+    private func makeProStatusMenuItem() -> NSMenuItem {
+        let item = NSMenuItem(
+            title: "",
+            action: #selector(openProStatusFromStatusMenu(_:)),
+            keyEquivalent: ""
+        )
+        item.identifier = Self.proStatusMenuItemIdentifier
+        item.target = self
+        configureProStatusMenuItem(item)
+        return item
+    }
+
+    private func configureProStatusMenuItem(in menu: NSMenu) {
+        guard let item = menu.items.first(where: { $0.identifier == Self.proStatusMenuItemIdentifier }) else {
+            return
+        }
+        configureProStatusMenuItem(item)
+    }
+
+    private func configureProStatusMenuItem(_ item: NSMenuItem) {
+        let isUnlocked = ProEntitlementPolicy.accessState().isUnlocked
+        item.title = tr(isUnlocked ? "menu.proStatus.unlocked" : "menu.proStatus.free")
+        item.action = #selector(openProStatusFromStatusMenu(_:))
+        item.target = self
+        item.isEnabled = true
+        item.image = isUnlocked ? makeProStatusMenuIcon() : nil
+    }
+
+    private func makeProStatusMenuIcon() -> NSImage? {
+        guard let symbol = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: "Pro") else {
+            return nil
+        }
+        let configuration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
+        let configuredSymbol = symbol.withSymbolConfiguration(configuration) ?? symbol
+        let image = NSImage(size: NSSize(width: 16, height: 16))
+        image.lockFocus()
+        NSColor(red: 0.96, green: 0.63, blue: 0.10, alpha: 1.0).set()
+        let iconRect = NSRect(x: 1.5, y: 1.5, width: 13, height: 13)
+        configuredSymbol.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0)
+        NSColor(red: 0.96, green: 0.63, blue: 0.10, alpha: 1.0).set()
+        iconRect.fill(using: .sourceAtop)
+        image.unlockFocus()
+        image.isTemplate = false
+        image.accessibilityDescription = "Pro"
+        return image
+    }
+
     private var showAppListMenuTitle: String {
         if LauncherHotkeyRegistrationStore.state(for: .main) == .failed {
             return tr("menu.showShortcutUnavailable")
         }
-        return "\(tr("menu.showAppList"))  \(LauncherHotkey.main.displayString)"
+        return "\(tr("menu.showAppList"))  \(LauncherHotkeySettings.effectiveHotkey(for: .main).displayString)"
     }
 
     private func observeApplicationMenuChanges() {
@@ -1094,14 +1281,106 @@
     // MARK: - Global Hotkeys
 
     private func registerConfiguredHotkeys() {
+        guard !configuredHotkeysSuspendedForRecording else { return }
         installHotkeyEventHandlerIfNeeded()
-        registerFixedHotkey(.main)
-        registerFixedHotkey(.quickSearch)
+        registerEffectiveHotkey(.main)
+        registerEffectiveHotkey(.quickSearch)
     }
 
-    private func registerFixedHotkey(_ kind: LauncherHotkeyKind) {
+    func suspendConfiguredHotkeysForRecording() {
+        guard !configuredHotkeysSuspendedForRecording else { return }
+        configuredHotkeysSuspendedForRecording = true
+        unregisterHotkey(for: .main)
+        unregisterHotkey(for: .quickSearch)
+    }
+
+    func resumeConfiguredHotkeysAfterRecording() {
+        guard configuredHotkeysSuspendedForRecording else { return }
+        configuredHotkeysSuspendedForRecording = false
+        registerConfiguredHotkeys()
+    }
+
+    private func registerEffectiveHotkey(_ kind: LauncherHotkeyKind) {
         unregisterHotkey(for: kind)
-        let hotkey = kind.hotkey
+        let hotkey = LauncherHotkeySettings.effectiveHotkey(for: kind)
+        registerAndStoreHotkey(hotkey, for: kind, markFailure: true)
+    }
+
+    @discardableResult
+    func applyCustomHotkey(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult {
+        guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
+            return .locked
+        }
+        if let error = LauncherHotkeySettings.validationError(for: hotkey, kind: kind) {
+            return .invalid(error)
+        }
+        return activateHotkey(hotkey, for: kind, persistCustom: true)
+    }
+
+    @discardableResult
+    func restoreDefaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult {
+        guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
+            return .locked
+        }
+        return activateHotkey(LauncherHotkeySettings.defaultHotkey(for: kind), for: kind, persistCustom: false)
+    }
+
+    private func activateHotkey(
+        _ hotkey: LauncherHotkey,
+        for kind: LauncherHotkeyKind,
+        persistCustom: Bool
+    ) -> LauncherHotkeyCustomizationResult {
+        installHotkeyEventHandlerIfNeeded()
+
+        if LauncherHotkeySettings.effectiveHotkey(for: kind) == hotkey,
+           hotkeyRef(for: kind) != nil {
+            if persistCustom {
+                LauncherHotkeySettings.save(hotkey, for: kind)
+                LauncherHotkeyRegistrationStore.setActive(for: kind)
+                return .saved
+            }
+            LauncherHotkeySettings.clearCustomHotkey(for: kind)
+            LauncherHotkeyRegistrationStore.setActive(for: kind)
+            return .restored
+        }
+
+        switch tryRegisterHotkey(hotkey, for: kind, markFailure: false) {
+        case .success(let newRef):
+            if let oldRef = hotkeyRef(for: kind) {
+                UnregisterEventHotKey(oldRef)
+            }
+            setHotkeyRef(newRef, for: kind)
+            if persistCustom {
+                LauncherHotkeySettings.save(hotkey, for: kind)
+                LauncherHotkeyRegistrationStore.setActive(for: kind)
+                return .saved
+            }
+            LauncherHotkeySettings.clearCustomHotkey(for: kind)
+            LauncherHotkeyRegistrationStore.setActive(for: kind)
+            return .restored
+        case .failure(let status):
+            return .registrationFailed(status)
+        }
+    }
+
+    private func registerAndStoreHotkey(
+        _ hotkey: LauncherHotkey,
+        for kind: LauncherHotkeyKind,
+        markFailure: Bool
+    ) {
+        switch tryRegisterHotkey(hotkey, for: kind, markFailure: markFailure) {
+        case .success(let newRef):
+            setHotkeyRef(newRef, for: kind)
+        case .failure:
+            setHotkeyRef(nil, for: kind)
+        }
+    }
+
+    private func tryRegisterHotkey(
+        _ hotkey: LauncherHotkey,
+        for kind: LauncherHotkeyKind,
+        markFailure: Bool
+    ) -> HotkeyRegistrationAttempt {
 
         var hotkeyID = EventHotKeyID()
         hotkeyID.signature = OSType(0x41505447) // 'APTG'
@@ -1118,12 +1397,16 @@
         )
 
         if status == noErr, let newRef {
-            setHotkeyRef(newRef, for: kind)
-            LauncherHotkeyRegistrationStore.setActive(for: kind)
+            if markFailure {
+                LauncherHotkeyRegistrationStore.setActive(for: kind)
+            }
+            return .success(newRef)
         } else {
-            setHotkeyRef(nil, for: kind)
-            LauncherHotkeyRegistrationStore.setFailed(status, for: kind)
+            if markFailure {
+                LauncherHotkeyRegistrationStore.setFailed(status, for: kind)
+            }
             print("[TagLauncher] Fixed hotkey registration failed for \(kind.rawValue): \(status)")
+            return .failure(status)
         }
     }
 
@@ -1133,6 +1416,18 @@
             object: nil,
             queue: .main
         ) { [weak self] _ in
+            self?.setupMenuBar()
+            self?.configureApplicationMenuWhenAvailable(retries: 2)
+        }
+    }
+
+    private func observeProEntitlementChanges() {
+        NotificationCenter.default.addObserver(
+            forName: .tagLauncherProEntitlementChanged,
+            object: nil,
+            queue: .main
+        ) { [weak self] _ in
+            self?.registerConfiguredHotkeys()
             self?.setupMenuBar()
             self?.configureApplicationMenuWhenAvailable(retries: 2)
         }
@@ -1189,9 +1484,27 @@
         ])
         if id == LauncherHotkeyKind.quickSearch.eventID {
             showQuickSearchFromGlobalHotkey()
+        } else if id == Self.overlaySpaceHotkeyID {
+            handleOverlaySpaceHotkey()
         } else {
             performToggleOverlay(preferredScreen: nil)
         }
+    }
+
+    private func handleOverlaySpaceHotkey() {
+        guard canUseOverlaySpaceHotkey else { return }
+        unregisterOverlaySpaceHotkey()
+        requestQuickSearch(source: QuickSearchOpenSource.mainOverlay)
+    }
+
+    private var canUseOverlaySpaceHotkey: Bool {
+        overlayWindow?.isVisible == true
+            && !isSettingsVisible
+            && !isQuickSearchOpen
+            && !quickSearchOnlyOverlaySession
+            && !isInEditMode
+            && !isEditingAppNote
+            && !isModalInteractionActive
     }
 
     private func showQuickSearchFromGlobalHotkey() {
@@ -1249,6 +1562,46 @@
         }
     }
 
+    private func refreshOverlaySpaceHotkeyRegistration() {
+        if canUseOverlaySpaceHotkey {
+            registerOverlaySpaceHotkeyIfNeeded()
+        } else {
+            unregisterOverlaySpaceHotkey()
+        }
+    }
+
+    private func registerOverlaySpaceHotkeyIfNeeded() {
+        guard overlaySpaceHotkeyRef == nil else { return }
+
+        var hotkeyID = EventHotKeyID()
+        hotkeyID.signature = OSType(0x41505447) // 'APTG'
+        hotkeyID.id = Self.overlaySpaceHotkeyID
+
+        var newRef: EventHotKeyRef?
+        let status = RegisterEventHotKey(
+            UInt32(kVK_Space),
+            0,
+            hotkeyID,
+            GetApplicationEventTarget(),
+            0,
+            &newRef
+        )
+
+        if status == noErr, let newRef {
+            overlaySpaceHotkeyRef = newRef
+        } else {
+            overlaySpaceHotkeyRef = nil
+            Diagnostics.log("app.overlay.spaceHotkey.failed", ["status": status])
+        }
+    }
+
+    private func unregisterOverlaySpaceHotkey() {
+        if let overlaySpaceHotkeyRef {
+            UnregisterEventHotKey(overlaySpaceHotkeyRef)
+            self.overlaySpaceHotkeyRef = nil
+        }
+    }
+
     // MARK: - Preferences
 
     /// Hide overlay when any other window becomes key (catches Cmd+, via SwiftUI Settings).
@@ -1295,15 +1648,22 @@
     /// Settings must always appear centered over the current overlay view and float above it.
     private func prepareSettingsWindow(_ window: NSWindow) {
         dismissQuickSearchIfNeeded()
-        let settingsSize = NSSize(width: 1000, height: 480)
+        let settingsSize = Self.settingsContentSize
+        let settingsFrameSize = window.frameRect(
+            forContentRect: NSRect(origin: .zero, size: settingsSize)
+        ).size
         window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
-        window.minSize = settingsSize
-        window.maxSize = settingsSize
-        if abs(window.frame.width - settingsSize.width) > 0.5 || abs(window.frame.height - settingsSize.height) > 0.5 {
-            window.setFrame(
-                NSRect(origin: window.frame.origin, size: settingsSize),
-                display: false
-            )
+        window.isRestorable = false
+        window.restorationClass = nil
+        _ = window.setFrameAutosaveName("")
+        window.contentMinSize = settingsSize
+        window.contentMaxSize = settingsSize
+        window.minSize = settingsFrameSize
+        window.maxSize = settingsFrameSize
+        let currentContentSize = window.contentView?.bounds.size ?? window.contentLayoutRect.size
+        if abs(currentContentSize.width - settingsSize.width) > 0.5
+            || abs(currentContentSize.height - settingsSize.height) > 0.5 {
+            window.setContentSize(settingsSize)
         }
 
         if let overlayWindow, overlayWindow.isVisible {
@@ -1328,6 +1688,7 @@
         window.makeKeyAndOrderFront(nil)
         window.orderFrontRegardless()
         settingsWindow = window
+        refreshOverlaySpaceHotkeyRegistration()
         refreshLauncherChromeState(
             activate: !overlayAvoidsSpaceSwitch,
             avoidSpaceSwitch: overlayAvoidsSpaceSwitch
@@ -1432,6 +1793,7 @@
                 activate: shouldRefocusOverlay && !self.overlayAvoidsSpaceSwitch,
                 avoidSpaceSwitch: self.overlayAvoidsSpaceSwitch
             )
+            self.refreshOverlaySpaceHotkeyRegistration()
             guard shouldRefocusOverlay else { return }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in
                 self?.showOrFocusOverlay(preferredScreen: preferredScreen)
@@ -1446,7 +1808,9 @@
             object: nil,
             queue: .main
         ) { [weak self] notification in
-            self?.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+            guard let self else { return }
+            self.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
     }
 
@@ -1463,6 +1827,7 @@
                 self.promoteOverlayToForegroundInput()
             }
             self.updateOverlayLevelForTextInput()
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
     }
 
@@ -1529,6 +1894,7 @@
                     self.hideOverlay(force: true, discardWindow: wasQuickSearchOnlyOverlay)
                 }
             }
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
 
         NotificationCenter.default.addObserver(
@@ -1536,7 +1902,9 @@
             object: nil,
             queue: .main
         ) { [weak self] notification in
-            self?.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+            guard let self else { return }
+            self.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
 
         NotificationCenter.default.addObserver(
@@ -1572,12 +1940,7 @@
         ) { [weak self] event in
             guard let self, self.isQuickSearchOpen else { return event }
             if event.window === self.overlayWindow {
-                NotificationCenter.default.post(
-                    name: .tagLauncherQuickSearchDismissRequested,
-                    object: nil,
-                    userInfo: ["source": QuickSearchDismissSource.mouse]
-                )
-                return nil
+                return event
             }
             return event
         }
@@ -1608,9 +1971,13 @@
             forName: .tagLauncherOpenPreferencesRequested,
             object: nil,
             queue: .main
-        ) { [weak self] _ in
-            self?.openPreferences()
+        ) { [weak self] notification in
+            self?.openPreferences(targetTab: Self.preferencesTabTarget(from: notification))
         }
+    }
+
+    private static func preferencesTabTarget(from notification: Notification) -> String? {
+        notification.userInfo?[SettingsTabTarget.userInfoKey] as? String
     }
 
     private func observeExternalActivationRequests() {
@@ -1632,6 +1999,15 @@
     }
 
     @objc private func openPreferences(_ sender: Any? = nil) {
+        openPreferences(targetTab: nil)
+    }
+
+    @objc private func openProStatusFromStatusMenu(_ sender: NSMenuItem) {
+        openPreferences(targetTab: SettingsTabTarget.pro)
+    }
+
+    private func openPreferences(targetTab: String?) {
+        explicitPreferencesOpenRequestedAt = Date()
         dismissQuickSearchIfNeeded()
         TagDatabase.flushPendingCategorySchemeBackupBatch()
         if overlayAvoidsSpaceSwitch {
@@ -1647,21 +2023,49 @@
 
         if let settingsWindow {
             prepareSettingsWindow(settingsWindow)
+            requestPreferencesTabSelection(targetTab)
             return
         }
 
-        let settingsSize = NSSize(width: 1000, height: 480)
         let window = NSWindow(
-            contentRect: NSRect(origin: .zero, size: settingsSize),
+            contentRect: NSRect(origin: .zero, size: Self.settingsContentSize),
             styleMask: [.titled, .closable],
             backing: .buffered,
             defer: false
         )
         window.title = tr("menu.preferences").replacingOccurrences(of: "…", with: "")
-        window.contentView = NSHostingView(rootView: PreferencesView())
+        window.contentView = NSHostingView(rootView: PreferencesView(initialTabRawValue: targetTab))
         window.isReleasedWhenClosed = false
         settingsWindow = window
         prepareSettingsWindow(window)
+    }
+
+    private func closeRestoredPreferencesWindowsDuringLaunch() {
+        for delay in [0.25, 0.8, 1.6, 2.6] {
+            DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
+                self?.closeRestoredPreferencesWindowIfNeeded()
+            }
+        }
+    }
+
+    private func closeRestoredPreferencesWindowIfNeeded() {
+        guard Date().timeIntervalSince(explicitPreferencesOpenRequestedAt) > 3.0 else { return }
+        for window in NSApp.windows where isSettingsWindowCandidate(window) {
+            detachSettingsWindow(window)
+            window.close()
+            if settingsWindow == window {
+                settingsWindow = nil
+            }
+        }
+    }
+
+    private func requestPreferencesTabSelection(_ targetTab: String?) {
+        guard let targetTab else { return }
+        NotificationCenter.default.post(
+            name: .tagLauncherPreferencesTabRequested,
+            object: nil,
+            userInfo: [SettingsTabTarget.userInfoKey: targetTab]
+        )
     }
 
     private func dismissQuickSearchIfNeeded() {
@@ -1735,7 +2139,13 @@
             super.mouseDown(with: event)
             return
         }
+        if routeUsageTipsMouseDownIfNeeded(event) {
+            return
+        }
         if hit == self {
+            if shouldSwallowUsageTipsBackdropClick(at: location) {
+                return
+            }
             if quickSearchSuppressesBackdropDismiss {
                 NotificationCenter.default.post(
                     name: .tagLauncherQuickSearchDismissRequested,
@@ -1770,6 +2180,32 @@
         super.mouseDown(with: event)
     }
 
+    private func routeUsageTipsMouseDownIfNeeded(_ event: NSEvent) -> Bool {
+        guard let appGridHost = findAppGridCollectionHost(in: self) else { return false }
+        return appGridHost.handleUsageTipsMouseDown(event)
+    }
+
+    private func findAppGridCollectionHost(in view: NSView) -> AppGridCollectionHostView? {
+        if let host = view as? AppGridCollectionHostView {
+            return host
+        }
+        for subview in view.subviews {
+            if let host = findAppGridCollectionHost(in: subview) {
+                return host
+            }
+        }
+        return nil
+    }
+
+    private func shouldSwallowUsageTipsBackdropClick(at location: NSPoint) -> Bool {
+        guard !UserDefaults.standard.bool(forKey: "hideUsageTips") else { return false }
+        let height = min(bounds.height, AppGridUsageTipsMetrics.reservedHeight)
+        guard height > 0 else { return false }
+        let y = isFlipped ? max(0, bounds.height - height) : 0
+        let region = NSRect(x: 0, y: y, width: bounds.width, height: height)
+        return region.contains(location)
+    }
+
     private func observeBackdropDismissSuppressionChanges() {
         modalInteractionObserver = NotificationCenter.default.addObserver(
             forName: .tagLauncherModalInteractionChanged,

--
Gitblit v1.9.3