| | |
| | | private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton") |
| | | private static let statusItemAccessibilityLabel = AppIdentity.displayName |
| | | private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem") |
| | | 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 launcherOverlayLevel = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue - 1) |
| | |
| | | } |
| | | } |
| | | |
| | | private func refreshLauncherChromeState(activate: Bool = false) { |
| | | private func refreshLauncherChromeState(activate: Bool = false, avoidSpaceSwitch: Bool = false) { |
| | | let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey) |
| | | lastShowDockIcon = showDock |
| | | |
| | | let desiredPolicy: NSApplication.ActivationPolicy = requiresForegroundOwnership |
| | | let shouldStayAccessoryForCurrentFullscreenSpace = avoidSpaceSwitch |
| | | && isOverlayVisible |
| | | && !isSettingsVisible |
| | | let desiredPolicy: NSApplication.ActivationPolicy = shouldStayAccessoryForCurrentFullscreenSpace |
| | | ? .accessory |
| | | : (requiresForegroundOwnership |
| | | ? .regular |
| | | : (showDock ? .regular : .accessory) |
| | | : (showDock ? .regular : .accessory)) |
| | | if NSApp.activationPolicy() != desiredPolicy { |
| | | NSApp.setActivationPolicy(desiredPolicy) |
| | | } |
| | |
| | | NSApp.presentationOptions = desiredPresentation |
| | | } |
| | | |
| | | if activate && requiresForegroundOwnership { |
| | | if activate && requiresForegroundOwnership && !shouldStayAccessoryForCurrentFullscreenSpace { |
| | | let keyWindow = isSettingsVisible ? settingsWindow : (isOverlayVisible ? overlayWindow : nil) |
| | | claimLauncherForeground( |
| | | keyWindow: keyWindow, |
| | |
| | | } |
| | | |
| | | self.configureApplicationMenu() |
| | | self.removeHelpMenu() |
| | | self.configureHelpMenu() |
| | | if retries > 0 && self.applicationMenuNeedsCleanup() { |
| | | self.configureApplicationMenuWhenAvailable(retries: retries - 1) |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private func removeHelpMenu() { |
| | | private func configureHelpMenu() { |
| | | guard let mainMenu = NSApp.mainMenu else { return } |
| | | NSApp.helpMenu = nil |
| | | |
| | | for item in mainMenu.items.reversed() { |
| | | let isHelpMenu = item.submenu === NSApp.helpMenu |
| | | let isHelpMenu = item.identifier == Self.helpMenuItemIdentifier |
| | | || item.title.localizedCaseInsensitiveContains("help") |
| | | || item.title == tr("menu.help") |
| | | || item.submenu?.title.localizedCaseInsensitiveContains("help") == true |
| | |
| | | mainMenu.removeItem(item) |
| | | } |
| | | } |
| | | |
| | | let menuItem = NSMenuItem(title: tr("menu.help"), action: nil, keyEquivalent: "") |
| | | let helpMenu = NSMenu(title: tr("menu.help")) |
| | | menuItem.identifier = Self.helpMenuItemIdentifier |
| | | menuItem.submenu = helpMenu |
| | | |
| | | let downloadItem = NSMenuItem() |
| | | downloadItem.identifier = Self.downloadHelpMenuItemIdentifier |
| | | downloadItem.title = tr("help.downloadPDF") |
| | | downloadItem.action = #selector(openLocalizedHelp(_:)) |
| | | downloadItem.target = self |
| | | downloadItem.keyEquivalent = "" |
| | | downloadItem.keyEquivalentModifierMask = [] |
| | | downloadItem.isEnabled = true |
| | | helpMenu.addItem(downloadItem) |
| | | mainMenu.addItem(menuItem) |
| | | } |
| | | |
| | | @objc private func openLocalizedHelp(_ sender: Any? = nil) { |
| | | NSWorkspace.shared.open(HelpDocument.currentURL) |
| | | } |
| | | |
| | | private func removeMenuBarItem() { |
| | |
| | | } |
| | | |
| | | private func showOrFocusOverlay(preferredScreen: NSScreen? = nil) { |
| | | let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen) |
| | | let shouldAvoidSpaceSwitch = placement.map { hasFullscreenWindowOnScreen($0.screen) } ?? false |
| | | if let overlayWindow, overlayWindow.isVisible { |
| | | moveOverlayToCurrentPlacement(preferredScreen: preferredScreen) |
| | | overlayWindow.level = currentOverlayLevel |
| | | refreshLauncherChromeState(activate: true) |
| | | refreshLauncherChromeState(activate: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch) |
| | | overlayWindow.makeKeyAndOrderFront(nil) |
| | | overlayWindow.orderFrontRegardless() |
| | | if let settingsWindow, settingsWindow.isVisible { |
| | |
| | | ) { |
| | | guard let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen) else { return } |
| | | |
| | | let shouldStageAsAccessory = shouldStageOverlayAsAccessory |
| | | let shouldAvoidSpaceSwitch = hasFullscreenWindowOnScreen(placement.screen) |
| | | let shouldStageAsAccessory = shouldStageOverlayAsAccessory || shouldAvoidSpaceSwitch |
| | | |
| | | if !stagedForAllSpaces && shouldStageAsAccessory { |
| | | // Let the all-spaces panel attach to the pointer's display before the app reclaims focus. |
| | |
| | | let placementFrame = placement.frame |
| | | let finishForegroundClaim: () -> Void = { [weak self, weak window] in |
| | | guard let self, let window, self.overlayWindow === window else { return } |
| | | self.refreshLauncherChromeState(activate: true) |
| | | self.refreshLauncherChromeState(activate: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch) |
| | | if window.frame != placementFrame { |
| | | window.setFrame(placementFrame, display: true) |
| | | window.makeKeyAndOrderFront(nil) |
| | |
| | | } else { |
| | | finishForegroundClaim() |
| | | } |
| | | } |
| | | |
| | | private func hasFullscreenWindowOnScreen(_ screen: NSScreen) -> Bool { |
| | | let screenFrame = screen.frame |
| | | let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? [] |
| | | return windows.contains { info in |
| | | guard let owner = info[kCGWindowOwnerName as String] as? String, |
| | | owner != AppIdentity.displayName, |
| | | owner != "Window Server", |
| | | owner != "Dock", |
| | | owner != "loginwindow", |
| | | let layer = info[kCGWindowLayer as String] as? Int, |
| | | layer == 0, |
| | | let bounds = info[kCGWindowBounds as String] as? NSDictionary |
| | | else { return false } |
| | | |
| | | let x = cgWindowDimension(bounds, "X") |
| | | let y = cgWindowDimension(bounds, "Y") |
| | | let width = cgWindowDimension(bounds, "Width") |
| | | let height = cgWindowDimension(bounds, "Height") |
| | | let windowFrame = NSRect(x: x, y: y, width: width, height: height) |
| | | let widthMatches = abs(windowFrame.width - screenFrame.width) <= 12 |
| | | let heightMatches = windowFrame.height >= screenFrame.height * 0.88 |
| | | let horizontallyAligned = abs(windowFrame.midX - screenFrame.midX) <= 12 |
| | | return widthMatches && heightMatches && horizontallyAligned |
| | | } |
| | | } |
| | | |
| | | private func cgWindowDimension(_ bounds: NSDictionary, _ key: String) -> CGFloat { |
| | | if let value = bounds[key] as? CGFloat { |
| | | return value |
| | | } |
| | | if let value = bounds[key] as? NSNumber { |
| | | return CGFloat(truncating: value) |
| | | } |
| | | return 0 |
| | | } |
| | | |
| | | private func installOverlayKeyMonitor() { |
| | |
| | | return |
| | | } |
| | | |
| | | if self.isMenuTrackingWindow(keyWindow) { |
| | | return |
| | | } |
| | | |
| | | if self.isAppOwnedDocumentWindow(keyWindow) { |
| | | self.prepareSettingsWindow(keyWindow) |
| | | return |
| | | } |
| | | |
| | | if NSApp.windows.contains(keyWindow) { |
| | | return |
| | | } |
| | | |
| | | // Settings/Preferences window -> float it above overlay for real-time preview. |
| | | if self.isSettingsWindowCandidate(keyWindow) { |
| | | self.prepareSettingsWindow(keyWindow) |
| | |
| | | private func isSettingsWindowCandidate(_ window: NSWindow) -> Bool { |
| | | if window == settingsWindow { return true } |
| | | if window.identifier?.rawValue == "TagLauncherPreferencesWindow" { return true } |
| | | if isAppOwnedDocumentWindow(window) { return true } |
| | | guard NSApp.windows.contains(window), |
| | | window != overlayWindow, |
| | | window.isVisible, |
| | |
| | | return settingsWindowTitleCandidates().contains(normalizedWindowTitle(window.title)) |
| | | } |
| | | |
| | | private func isAppOwnedDocumentWindow(_ window: NSWindow) -> Bool { |
| | | NSApp.windows.contains(window) |
| | | && window != overlayWindow |
| | | && window.isVisible |
| | | && !(window is NSPanel) |
| | | && !isMenuTrackingWindow(window) |
| | | && window.styleMask.contains(.titled) |
| | | } |
| | | |
| | | private func isMenuTrackingWindow(_ window: NSWindow) -> Bool { |
| | | let className = NSStringFromClass(type(of: window)) |
| | | return className.localizedCaseInsensitiveContains("Menu") |
| | | || className.localizedCaseInsensitiveContains("Popup") |
| | | } |
| | | |
| | | private func settingsWindowTitleCandidates() -> Set<String> { |
| | | let keys = [ |
| | | "menu.preferences", |