| | |
| | | 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") |
| | |
| | | func applicationDidFinishLaunching(_ notification: Notification) { |
| | | AppDefaults.register() |
| | | L10n.setup() |
| | | ProEntitlementCenter.shared.start() |
| | | migrateDefaultGroupName() |
| | | TagDatabase.seedDefaultTags() |
| | | _ = TagDatabase.relocalizeSystemTagsForCurrentLanguage() |
| | | syncChromeSettings(force: true) |
| | | observeHotkeyStatusChanges() |
| | | registerConfiguredHotkeys() |
| | |
| | | } |
| | | } |
| | | |
| | | 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) |
| | |
| | | overlayGeneration expectedOverlayGeneration: Int? = nil |
| | | ) { |
| | | if NSApp.activationPolicy() != .regular { |
| | | NSApp.setActivationPolicy(.regular) |
| | | setLauncherActivationPolicy(.regular) |
| | | } |
| | | |
| | | if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) { |
| | |
| | | ? .regular |
| | | : (showDock ? .regular : .accessory)))) |
| | | if NSApp.activationPolicy() != desiredPolicy { |
| | | NSApp.setActivationPolicy(desiredPolicy) |
| | | setLauncherActivationPolicy(desiredPolicy) |
| | | } |
| | | |
| | | let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : [] |
| | |
| | | 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 |
| | |
| | | |
| | | image.isTemplate = true |
| | | image.accessibilityDescription = "TagLauncher" |
| | | 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 |
| | | } |
| | | |
| | |
| | | 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() { |
| | |
| | | } |
| | | |
| | | @objc private func openPreferences(_ sender: Any? = nil) { |
| | | openPreferences(targetTab: nil) |
| | | } |
| | | |
| | | @objc private func openProStatusFromStatusMenu(_ sender: NSMenuItem) { |
| | | openPreferences(targetTab: SettingsTabTarget.about) |
| | | } |
| | | |
| | | private func openPreferences(targetTab: String?) { |
| | | dismissQuickSearchIfNeeded() |
| | | TagDatabase.flushPendingCategorySchemeBackupBatch() |
| | | if overlayAvoidsSpaceSwitch { |
| | |
| | | |
| | | if let settingsWindow { |
| | | prepareSettingsWindow(settingsWindow) |
| | | requestPreferencesTabSelection(targetTab) |
| | | return |
| | | } |
| | | |
| | |
| | | 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 requestPreferencesTabSelection(_ targetTab: String?) { |
| | | guard let targetTab else { return } |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherPreferencesTabRequested, |
| | | object: nil, |
| | | userInfo: [SettingsTabTarget.userInfoKey: targetTab] |
| | | ) |
| | | } |
| | | |
| | | private func dismissQuickSearchIfNeeded() { |
| | |
| | | super.mouseDown(with: event) |
| | | return |
| | | } |
| | | if routeUsageTipsMouseDownIfNeeded(event) { |
| | | return |
| | | } |
| | | if hit == self { |
| | | if shouldSwallowUsageTipsBackdropClick(at: location) { |
| | | return |
| | | } |
| | | if quickSearchSuppressesBackdropDismiss { |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherQuickSearchDismissRequested, |
| | |
| | | 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, |