| | |
| | | |
| | | // 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 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 |
| | |
| | | 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( |
| | |
| | | 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) |
| | | } |
| | | ) |
| | |
| | | func applicationDidFinishLaunching(_ notification: Notification) { |
| | | AppDefaults.register() |
| | | L10n.setup() |
| | | ProEntitlementCenter.shared.start() |
| | | migrateDefaultGroupName() |
| | | TagDatabase.seedDefaultTags() |
| | | _ = TagDatabase.relocalizeSystemTagsForCurrentLanguage() |
| | | syncChromeSettings(force: true) |
| | | observeHotkeyStatusChanges() |
| | | registerConfiguredHotkeys() |
| | |
| | | observeApplicationMenuChanges() |
| | | observeChromeSettings() |
| | | observeLanguageChanges() |
| | | observeProEntitlementChanges() |
| | | setupLaunchAtLogin() |
| | | suppressReopenUntil = Date().addingTimeInterval(1.0) |
| | | didFinishLaunching = true |
| | | closeRestoredPreferencesWindowsDuringLaunch() |
| | | consumePendingShowOverlayInvocationIfNeeded() |
| | | configureApplicationMenuWhenAvailable(retries: 200) |
| | | warmAppIndexInBackground() |
| | | relocalizeDefaultAppNotesForCurrentLanguageAsync() |
| | |
| | | guard isRecentUserClickNearDockArea() else { return false } |
| | | showOrFocusOverlay() |
| | | 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 { |
| | |
| | | hideOverlay(force: true, discardWindow: true) |
| | | unregisterHotkey(for: .main) |
| | | unregisterHotkey(for: .quickSearch) |
| | | unregisterOverlaySpaceHotkey() |
| | | TagDatabase.flushPendingCategorySchemeBackupBatch() |
| | | removeOverlayKeyMonitor() |
| | | removeQuickSearchExternalMouseMonitor() |
| | |
| | | |
| | | 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 |
| | | ) |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | versionItem.isEnabled = false |
| | | menu.addItem(versionItem) |
| | | |
| | | let proStatusItem = makeProStatusMenuItem() |
| | | menu.addItem(proStatusItem) |
| | | |
| | | menu.addItem(.separator()) |
| | | let prefsItem = NSMenuItem( |
| | | title: tr("menu.preferences"), |
| | |
| | | } |
| | | |
| | | func menuWillOpen(_ menu: NSMenu) { |
| | | configureProStatusMenuItem(in: menu) |
| | | statusMenuScreenForNextOverlay = overlayController.screenContainingCurrentPointer() |
| | | ?? statusItem?.button?.window?.screen |
| | | ?? NSScreen.main |
| | |
| | | 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() { |
| | |
| | | // 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' |
| | |
| | | ) |
| | | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | |
| | | 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) |
| | | } |
| | |
| | | ]) |
| | | 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() { |
| | |
| | | } |
| | | } |
| | | |
| | | 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). |
| | |
| | | forContentRect: NSRect(origin: .zero, size: settingsSize) |
| | | ).size |
| | | window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow") |
| | | window.isRestorable = false |
| | | window.restorationClass = nil |
| | | _ = window.setFrameAutosaveName("") |
| | | window.contentMinSize = settingsSize |
| | | window.contentMaxSize = settingsSize |
| | | window.minSize = settingsFrameSize |
| | |
| | | window.makeKeyAndOrderFront(nil) |
| | | window.orderFrontRegardless() |
| | | settingsWindow = window |
| | | refreshOverlaySpaceHotkeyRegistration() |
| | | refreshLauncherChromeState( |
| | | activate: !overlayAvoidsSpaceSwitch, |
| | | avoidSpaceSwitch: overlayAvoidsSpaceSwitch |
| | |
| | | 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) |
| | |
| | | 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() |
| | | } |
| | | } |
| | | |
| | |
| | | self.promoteOverlayToForegroundInput() |
| | | } |
| | | self.updateOverlayLevelForTextInput() |
| | | self.refreshOverlaySpaceHotkeyRegistration() |
| | | } |
| | | } |
| | | |
| | |
| | | self.hideOverlay(force: true, discardWindow: wasQuickSearchOnlyOverlay) |
| | | } |
| | | } |
| | | self.refreshOverlaySpaceHotkeyRegistration() |
| | | } |
| | | |
| | | NotificationCenter.default.addObserver( |
| | |
| | | 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( |
| | |
| | | ) { [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 |
| | | } |
| | |
| | | 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 { |
| | |
| | | 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?) { |
| | |
| | | // NSHostingView.hitTest may return a SwiftUI-internal wrapper — the |
| | | // actual AppKit subview may be nested deeper. |
| | | if let container = findTextFieldContainer(in: hit) { |
| | | container.mouseDown(with: event) |
| | | container.focusTextField() |
| | | return |
| | | } |
| | | if let tf = findNSTextField(in: hit) { |
| | | let shouldSelectAll = tf.currentEditor() == nil |
| | | tf.window?.makeFirstResponder(tf) |
| | | tf.mouseDown(with: event) |
| | | if shouldSelectAll { |
| | | tf.selectText(nil) |
| | | } |
| | | return |
| | | } |
| | | super.mouseDown(with: event) |