Ariver
2026-05-20 7c97841d07d4a596508f663c887d1f67d105faf6
Apptag/ApptagApp.swift
@@ -33,15 +33,19 @@
    private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton")
    private static let statusItemAccessibilityLabel = "TagLauncher"
    private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem")
    private static let showAppListShortcutGlyphs = "⌥⇧␣"
    private static let overlayDefaultLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
    private static let overlayTextInputLevel = NSWindow.Level.modalPanel
    private var statusItem: NSStatusItem?
    private var overlayWindow: NSWindow?
    private var overlayKeyMonitor: Any?
    private var quickSearchExternalMouseMonitor: Any?
    private var settingsWindow: NSWindow?    // Track Settings window to keep it above overlay
    private var hotkeyRef: EventHotKeyRef?
    private var mainHotkeyRef: EventHotKeyRef?
    private var quickSearchHotkeyRef: EventHotKeyRef?
    private var hotkeyEventHandlerInstalled = false
    private var isQuickSearchOpen = false
    private var isModalInteractionActive = false
    private var isInEditMode = false  // Suppress auto-dismiss during editing
    private var isEditingAppNote = false
    private var isConfiguringApplicationMenu = false
@@ -61,11 +65,13 @@
        migrateDefaultGroupName()
        TagDatabase.seedDefaultTags()
        syncChromeSettings(force: true)
        registerHotkey()
        observeHotkeyStatusChanges()
        registerConfiguredHotkeys()
        observeOtherWindows()
        observeSettingsClose()
        observeEditMode()
        observeAppNoteEditing()
        observeQuickSearch()
        observePreferencesRequests()
        observeApplicationMenuChanges()
        observeChromeSettings()
@@ -82,6 +88,7 @@
    func applicationWillTerminate(_ notification: Notification) {
        TagDatabase.flushPendingCategorySchemeBackupBatch()
        removeQuickSearchExternalMouseMonitor()
    }
    /// Ensure defaultGroupName is always the language-neutral key "Other".
@@ -333,7 +340,10 @@
    }
    private var showAppListMenuTitle: String {
        "\(tr("menu.showAppList"))  \(Self.showAppListShortcutGlyphs)"
        if LauncherHotkeyRegistrationStore.state(for: .main) == .failed {
            return tr("menu.showShortcutUnavailable")
        }
        return "\(tr("menu.showAppList"))  \(LauncherHotkey.main.displayString)"
    }
    private func observeApplicationMenuChanges() {
@@ -502,7 +512,7 @@
        }
    }
    private func showOverlay() {
    private func showOverlay(initialQuickSearchSource: String? = nil) {
        // Use the screen under the mouse cursor — works in fullscreen spaces
        let mousePoint = NSEvent.mouseLocation
        guard let screen = NSScreen.screens.first(where: {
@@ -513,7 +523,7 @@
        if let existingWindow = overlayWindow {
            window = existingWindow
        } else {
            window = makeOverlayWindow(on: screen)
            window = makeOverlayWindow(on: screen, initialQuickSearchSource: initialQuickSearchSource)
            overlayWindow = window
        }
@@ -521,6 +531,14 @@
        window.level = isEditingAppNote ? Self.overlayTextInputLevel : Self.overlayDefaultLevel
        installOverlayKeyMonitor()
        if let initialQuickSearchSource {
            NotificationCenter.default.post(
                name: .tagLauncherQuickSearchRequested,
                object: nil,
                userInfo: ["source": initialQuickSearchSource]
            )
        }
        window.makeKeyAndOrderFront(nil)
        window.orderFrontRegardless()
@@ -530,12 +548,58 @@
    private func installOverlayKeyMonitor() {
        guard overlayKeyMonitor == nil else { return }
        overlayKeyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
            guard let self else { return event }
            if event.keyCode == 53 { // Escape
                self?.hideOverlay()
                if self.isQuickSearchOpen {
                    NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
                    return nil
                }
                self.hideOverlay()
                return nil
            }
            if self.shouldOpenQuickSearch(for: event) {
                NotificationCenter.default.post(
                    name: .tagLauncherQuickSearchRequested,
                    object: nil,
                    userInfo: ["source": QuickSearchOpenSource.mainOverlay]
                )
                return nil
            }
            return event
        }
    }
    private func shouldOpenQuickSearch(for event: NSEvent) -> Bool {
        guard event.keyCode == UInt16(kVK_Space),
              !event.isARepeat,
              event.modifierFlags.intersection(.deviceIndependentFlagsMask).isEmpty,
              overlayWindow?.isVisible == true,
              !isQuickSearchOpen,
              !isInEditMode,
              !isEditingAppNote,
              !isModalInteractionActive
        else { return false }
        guard let firstResponder = overlayWindow?.firstResponder else { return true }
        if firstResponder is NSText || firstResponder is NSTextField {
            return false
        }
        if let responderView = firstResponder as? NSView,
           viewOrAncestorHandlesSpace(responderView) {
            return false
        }
        return true
    }
    private func viewOrAncestorHandlesSpace(_ view: NSView) -> Bool {
        var current: NSView? = view
        while let candidate = current {
            if candidate is NSButton || candidate is NSSegmentedControl || candidate is NSSlider {
                return true
            }
            current = candidate.superview
        }
        return false
    }
    private func removeOverlayKeyMonitor() {
@@ -545,7 +609,7 @@
        }
    }
    private func makeOverlayWindow(on screen: NSScreen) -> NSWindow {
    private func makeOverlayWindow(on screen: NSScreen, initialQuickSearchSource: String? = nil) -> NSWindow {
        let panel = OverlayPanel(
            contentRect: screen.frame,
            styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
@@ -568,9 +632,12 @@
        panel.titleVisibility = .hidden
        panel.isReleasedWhenClosed = false
        panel.contentView = DismissibleHostingView(
            rootView: ContentView(hideOverlay: { [weak self] in
                self?.hideOverlay(force: true)
            }),
            rootView: ContentView(
                hideOverlay: { [weak self] in
                    self?.hideOverlay(force: true)
                },
                initialQuickSearchSource: initialQuickSearchSource
            ),
            onBackdropTap: { [weak self] in
                self?.hideOverlay()
            }
@@ -586,54 +653,91 @@
        }
        overlayWindow?.orderOut(nil)
        removeOverlayKeyMonitor()
        removeQuickSearchExternalMouseMonitor()
        NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
        if force {
            overlayWindow = nil
        }
    }
    // MARK: - Global Hotkey (Shift+Option+Space)
    // MARK: - Global Hotkeys
    /// Carbon RegisterEventHotKey. If it fails (sandbox, etc.), falls back to menu bar only.
    private func registerHotkey() {
    private func registerConfiguredHotkeys() {
        installHotkeyEventHandlerIfNeeded()
        registerFixedHotkey(.main)
        registerFixedHotkey(.quickSearch)
    }
    private func registerFixedHotkey(_ kind: LauncherHotkeyKind) {
        unregisterHotkey(for: kind)
        let hotkey = kind.hotkey
        var hotkeyID = EventHotKeyID()
        hotkeyID.signature = OSType(0x41505447) // 'APTG'
        hotkeyID.id = 1
        hotkeyID.id = kind.eventID
        let modifiers = UInt32(shiftKey | optionKey)
        var ref: EventHotKeyRef?
        var newRef: EventHotKeyRef?
        let status = RegisterEventHotKey(
            UInt32(kVK_Space),
            modifiers,
            hotkey.keyCode,
            hotkey.modifiers,
            hotkeyID,
            GetApplicationEventTarget(),
            0,
            &ref
            &newRef
        )
        hotkeyRef = ref
        if status != noErr {
            print("[TagLauncher] Hotkey registration failed: \(status). Falling back to menu bar only.")
            return
        if status == noErr, let newRef {
            setHotkeyRef(newRef, for: kind)
            LauncherHotkeyRegistrationStore.setActive(for: kind)
        } else {
            setHotkeyRef(nil, for: kind)
            LauncherHotkeyRegistrationStore.setFailed(status, for: kind)
            print("[TagLauncher] Fixed hotkey registration failed for \(kind.rawValue): \(status)")
        }
    }
    private func observeHotkeyStatusChanges() {
        NotificationCenter.default.addObserver(
            forName: .tagLauncherHotkeyRegistrationChanged,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.setupMenuBar()
            self?.configureApplicationMenuWhenAvailable(retries: 2)
        }
    }
    private func installHotkeyEventHandlerIfNeeded() {
        guard !hotkeyEventHandlerInstalled else { return }
        hotkeyEventHandlerInstalled = true
        var eventSpec = EventTypeSpec(
            eventClass: OSType(kEventClassKeyboard),
            eventKind: UInt32(kEventHotKeyPressed)
        )
        let selfPtr = Unmanaged.passUnretained(self).toOpaque()
        InstallEventHandler(
            GetApplicationEventTarget(),
            { (_, _, userData) -> OSStatus in
                guard let userData else { return noErr }
            { (_, event, userData) -> OSStatus in
                guard let event, let userData else { return noErr }
                var hotkeyID = EventHotKeyID()
                let status = GetEventParameter(
                    event,
                    EventParamName(kEventParamDirectObject),
                    EventParamType(typeEventHotKeyID),
                    nil,
                    MemoryLayout<EventHotKeyID>.size,
                    nil,
                    &hotkeyID
                )
                guard status == noErr else { return noErr }
                let delegate = Unmanaged<AppDelegate>
                    .fromOpaque(userData)
                    .takeUnretainedValue()
                DispatchQueue.main.async {
                    delegate.toggleOverlay()
                    delegate.handleHotkeyEvent(id: hotkeyID.id)
                }
                return noErr
            },
@@ -642,6 +746,40 @@
            selfPtr,
            nil
        )
    }
    private func handleHotkeyEvent(id: UInt32) {
        if id == LauncherHotkeyKind.quickSearch.eventID {
            showQuickSearchFromGlobalHotkey()
        } else {
            toggleOverlay()
        }
    }
    private func showQuickSearchFromGlobalHotkey() {
        guard overlayWindow?.isVisible != true else { return }
        showOverlay(initialQuickSearchSource: QuickSearchOpenSource.globalHidden)
    }
    private func hotkeyRef(for kind: LauncherHotkeyKind) -> EventHotKeyRef? {
        switch kind {
        case .main: return mainHotkeyRef
        case .quickSearch: return quickSearchHotkeyRef
        }
    }
    private func setHotkeyRef(_ ref: EventHotKeyRef?, for kind: LauncherHotkeyKind) {
        switch kind {
        case .main: mainHotkeyRef = ref
        case .quickSearch: quickSearchHotkeyRef = ref
        }
    }
    private func unregisterHotkey(for kind: LauncherHotkeyKind) {
        if let ref = hotkeyRef(for: kind) {
            UnregisterEventHotKey(ref)
            setHotkeyRef(nil, for: kind)
        }
    }
    // MARK: - Preferences
@@ -780,6 +918,58 @@
        }
    }
    private func observeQuickSearch() {
        NotificationCenter.default.addObserver(
            forName: .tagLauncherQuickSearchVisibilityChanged,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            guard let self else { return }
            self.isQuickSearchOpen = (notification.userInfo?["active"] as? Bool) ?? false
            if self.isQuickSearchOpen {
                self.installQuickSearchExternalMouseMonitor()
            } else {
                self.removeQuickSearchExternalMouseMonitor()
            }
        }
        NotificationCenter.default.addObserver(
            forName: .tagLauncherModalInteractionChanged,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            self?.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
        }
        NotificationCenter.default.addObserver(
            forName: NSApplication.didResignActiveNotification,
            object: NSApp,
            queue: .main
        ) { [weak self] _ in
            guard self?.isQuickSearchOpen == true else { return }
            NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
        }
    }
    private func installQuickSearchExternalMouseMonitor() {
        guard quickSearchExternalMouseMonitor == nil else { return }
        quickSearchExternalMouseMonitor = NSEvent.addGlobalMonitorForEvents(
            matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown]
        ) { [weak self] _ in
            DispatchQueue.main.async {
                guard self?.isQuickSearchOpen == true else { return }
                NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
            }
        }
    }
    private func removeQuickSearchExternalMouseMonitor() {
        if let quickSearchExternalMouseMonitor {
            NSEvent.removeMonitor(quickSearchExternalMouseMonitor)
            self.quickSearchExternalMouseMonitor = nil
        }
    }
    private func updateOverlayLevelForTextInput() {
        guard let overlayWindow else { return }
        overlayWindow.level = isEditingAppNote ? Self.overlayTextInputLevel : Self.overlayDefaultLevel