| | |
| | | Settings { |
| | | PreferencesView() |
| | | } |
| | | .defaultSize(width: 1000, height: 480) |
| | | .defaultSize(width: 1000, height: 640) |
| | | .commands { |
| | | CommandGroup(replacing: .systemServices) { } |
| | | CommandGroup(replacing: .appVisibility) { } |
| | |
| | | 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? |
| | |
| | | .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 |
| | |
| | | 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 |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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] : [] |
| | |
| | | /// 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.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 { |
| | |
| | | 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) |
| | | } |
| | | |
| | | private func openPreferences(targetTab: String?) { |
| | | dismissQuickSearchIfNeeded() |
| | | TagDatabase.flushPendingCategorySchemeBackupBatch() |
| | | if overlayAvoidsSpaceSwitch { |
| | |
| | | |
| | | 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 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, |