From 65b66f723294dd02174d30a67a05b88139f274ee Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 29 May 2026 05:12:04 +0800
Subject: [PATCH] Extract overlay window controller
---
Apptag/ApptagApp.swift | 366 +++++++++++----------------------------------------
1 files changed, 81 insertions(+), 285 deletions(-)
diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index 3a5f1d7..7f0fabc 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -28,15 +28,6 @@
// MARK: - App Delegate (menubar + overlay window + hotkey)
-final class OverlayPanel: NSPanel {
- override var canBecomeKey: Bool { true }
- override var canBecomeMain: Bool { true }
-
- override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
- frameRect
- }
-}
-
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
private static let showDockIconKey = "showDockIcon"
private static let statusItemAutosaveName = AppIdentity.statusItemAutosaveName
@@ -52,7 +43,6 @@
private static let overlayTextInputLevel = launcherOverlayLevel
private var statusItem: NSStatusItem?
- private var overlayWindow: NSWindow?
private var overlayKeyMonitor: Any?
private var quickSearchLocalMouseMonitor: Any?
private var quickSearchExternalMouseMonitor: Any?
@@ -67,18 +57,82 @@
private var isConfiguringApplicationMenu = false
private var lastShowDockIcon: Bool?
private var statusMenuScreenForNextOverlay: NSScreen?
- private var overlayGeneration = 0
- private var overlayAvoidsSpaceSwitch = false
private var suppressReopenUntil = Date.distantPast
- private struct OverlayPlacementContext {
- let screen: NSScreen
- let frame: NSRect
+ private lazy var overlayController = OverlayWindowController(
+ dependencies: OverlayWindowController.Dependencies(
+ shouldStageAsAccessory: { [weak self] in
+ self?.shouldStageOverlayAsAccessory ?? false
+ },
+ isSettingsVisible: { [weak self] in
+ self?.isSettingsVisible ?? false
+ },
+ settingsWindow: { [weak self] in
+ self?.settingsWindow
+ },
+ canHideOverlay: { [weak self] in
+ self?.isInEditMode == false
+ },
+ currentOverlayLevel: { [weak self] in
+ self?.currentOverlayLevel ?? Self.overlayDefaultLevel
+ },
+ overlayLevel: { [weak self] initialQuickSearchSource in
+ self?.overlayLevel(initialQuickSearchSource: initialQuickSearchSource) ?? Self.overlayDefaultLevel
+ },
+ makeContentView: { [weak self] initialQuickSearchSource in
+ DismissibleHostingView(
+ rootView: ContentView(
+ hideOverlay: { [weak self] in
+ self?.hideOverlay(force: true)
+ },
+ initialQuickSearchSource: initialQuickSearchSource
+ ),
+ onBackdropTap: { [weak self] in
+ self?.hideOverlay()
+ }
+ )
+ },
+ installOverlayKeyMonitor: { [weak self] in
+ self?.installOverlayKeyMonitor()
+ },
+ removeOverlayKeyMonitor: { [weak self] in
+ self?.removeOverlayKeyMonitor()
+ },
+ removeQuickSearchMouseMonitor: { [weak self] in
+ self?.removeQuickSearchExternalMouseMonitor()
+ },
+ detachSettingsWindow: { [weak self] window in
+ self?.detachSettingsWindow(window)
+ },
+ prepareSettingsWindow: { [weak self] window in
+ self?.prepareSettingsWindow(window)
+ },
+ refreshChromeState: { [weak self] activate, avoidSpaceSwitch in
+ self?.refreshLauncherChromeState(activate: activate, avoidSpaceSwitch: avoidSpaceSwitch)
+ },
+ onWillHide: {
+ TagDatabase.flushPendingCategorySchemeBackupBatch()
+ },
+ onDidHide: {
+ NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
+ },
+ onDidShow: {
+ NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
+ }
+ )
+ )
+
+ private var overlayWindow: NSWindow? {
+ overlayController.window
}
- private struct ForeignWindowFrame {
- let owner: String
- let frame: NSRect
+ private var overlayGeneration: Int {
+ overlayController.generation
+ }
+
+ private var overlayAvoidsSpaceSwitch: Bool {
+ get { overlayController.avoidsSpaceSwitch }
+ set { overlayController.avoidsSpaceSwitch = newValue }
}
private var isOverlayVisible: Bool {
@@ -487,7 +541,7 @@
}
func menuWillOpen(_ menu: NSMenu) {
- statusMenuScreenForNextOverlay = screenContainingCurrentPointer()
+ statusMenuScreenForNextOverlay = overlayController.screenContainingCurrentPointer()
?? statusItem?.button?.window?.screen
?? NSScreen.main
?? NSScreen.screens.first
@@ -774,37 +828,11 @@
}
private func performToggleOverlay(preferredScreen: NSScreen?) {
- if overlayWindow?.isVisible == true {
- hideOverlay(force: true)
- } else {
- showOverlay(preferredScreen: preferredScreen)
- }
+ overlayController.toggle(preferredScreen: preferredScreen)
}
private func showOrFocusOverlay(preferredScreen: NSScreen? = nil) {
- let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen)
- let shouldAvoidSpaceSwitch = placement.map { hasFullscreenWindowOnScreen($0.screen) } ?? false
- if let overlayWindow, overlayWindow.isVisible {
- overlayAvoidsSpaceSwitch = shouldAvoidSpaceSwitch
- moveOverlayToCurrentPlacement(preferredScreen: preferredScreen)
- overlayWindow.level = currentOverlayLevel
- refreshLauncherChromeState(activate: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch)
- overlayWindow.makeKeyAndOrderFront(nil)
- overlayWindow.orderFrontRegardless()
- if let settingsWindow, settingsWindow.isVisible {
- prepareSettingsWindow(settingsWindow)
- }
- return
- }
- showOverlay(preferredScreen: preferredScreen)
- }
-
- private func moveOverlayToCurrentPlacement(preferredScreen: NSScreen?) {
- guard let overlayWindow,
- let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen),
- overlayWindow.frame != placement.frame
- else { return }
- overlayWindow.setFrame(placement.frame, display: true)
+ overlayController.showOrFocus(preferredScreen: preferredScreen)
}
private func showOverlay(
@@ -812,174 +840,11 @@
preferredScreen: NSScreen? = nil,
stagedForAllSpaces: Bool = false
) {
- guard let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen) else { return }
-
- 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.
- if NSApp.activationPolicy() != .accessory {
- NSApp.setActivationPolicy(.accessory)
- }
- if NSApp.isActive {
- NSApp.deactivate()
- }
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in
- self?.showOverlay(
- initialQuickSearchSource: initialQuickSearchSource,
- preferredScreen: placement.screen,
- stagedForAllSpaces: true
- )
- }
- return
- }
-
- if overlayWindow?.isVisible == true {
- hideOverlay(force: true)
- } else if let existingWindow = overlayWindow {
- existingWindow.orderOut(nil)
- overlayWindow = nil
- }
- overlayAvoidsSpaceSwitch = shouldAvoidSpaceSwitch
-
- let window = makeOverlayWindow(
- on: placement.screen,
- initialQuickSearchSource: initialQuickSearchSource
+ overlayController.show(
+ initialQuickSearchSource: initialQuickSearchSource,
+ preferredScreen: preferredScreen,
+ stagedForAllSpaces: stagedForAllSpaces
)
- overlayGeneration &+= 1
- overlayWindow = window
- installOverlayKeyMonitor()
- if shouldStageAsAccessory {
- if NSApp.isActive {
- NSApp.deactivate()
- }
- if NSApp.activationPolicy() != .accessory {
- NSApp.setActivationPolicy(.accessory)
- }
- }
-
- let targetLevel = overlayLevel(initialQuickSearchSource: initialQuickSearchSource)
- window.setFrame(placement.frame, display: true)
- window.level = targetLevel
-
- window.makeKeyAndOrderFront(nil)
- window.orderFrontRegardless()
-
- 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: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch)
- if window.frame != placementFrame {
- window.setFrame(placementFrame, display: true)
- window.makeKeyAndOrderFront(nil)
- window.orderFrontRegardless()
- }
- if let settingsWindow = self.settingsWindow, settingsWindow.isVisible {
- self.prepareSettingsWindow(settingsWindow)
- }
- NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
- }
-
- if !isSettingsVisible {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: finishForegroundClaim)
- } else {
- finishForegroundClaim()
- }
- }
-
- private func hasFullscreenWindowOnScreen(_ screen: NSScreen) -> Bool {
- let screenFrame = screen.frame
- let windowFrames = foreignLayerZeroWindows(on: screenFrame)
-
- if windowFrames.contains(where: { isSingleFullscreenWindow($0.frame, on: screenFrame) }) {
- return true
- }
-
- return hasSplitViewFullscreenWindows(windowFrames, on: screenFrame)
- }
-
- private func foreignLayerZeroWindows(on screenFrame: NSRect) -> [ForeignWindowFrame] {
- let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
- return windows.compactMap { 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 nil }
-
- let frame = NSRect(
- x: cgWindowDimension(bounds, "X"),
- y: cgWindowDimension(bounds, "Y"),
- width: cgWindowDimension(bounds, "Width"),
- height: cgWindowDimension(bounds, "Height")
- )
- guard frame.intersects(screenFrame) else { return nil }
- return ForeignWindowFrame(owner: owner, frame: frame)
- }
- }
-
- private func isSingleFullscreenWindow(_ windowFrame: NSRect, on screenFrame: NSRect) -> Bool {
- let widthMatches = abs(windowFrame.width - screenFrame.width) <= 12
- let heightMatches = windowFrame.height >= screenFrame.height * 0.88
- let horizontallyAligned = abs(windowFrame.midX - screenFrame.midX) <= 12
- let verticallyAligned = abs(windowFrame.maxY - screenFrame.maxY) <= 32
- return widthMatches && heightMatches && horizontallyAligned && verticallyAligned
- }
-
- private func hasSplitViewFullscreenWindows(_ windows: [ForeignWindowFrame], on screenFrame: NSRect) -> Bool {
- let clippedWindows = windows.map {
- ForeignWindowFrame(owner: $0.owner, frame: $0.frame.intersection(screenFrame))
- }
- let tallWindows = clippedWindows
- .filter { window in
- let frame = window.frame
- return frame.height >= screenFrame.height * 0.86
- && frame.width >= screenFrame.width * 0.20
- && frame.width <= screenFrame.width * 0.86
- && abs(frame.maxY - screenFrame.maxY) <= 32
- }
- .sorted { $0.frame.minX < $1.frame.minX }
-
- guard tallWindows.count >= 2 else { return false }
-
- for startIndex in tallWindows.indices {
- var union = tallWindows[startIndex].frame
- var lastMaxX = union.maxX
-
- for window in tallWindows.dropFirst(startIndex + 1) {
- let gap = window.frame.minX - lastMaxX
- if gap < -32 || gap > 48 {
- break
- }
- union = union.union(window.frame)
- lastMaxX = max(lastMaxX, window.frame.maxX)
-
- let touchesLeft = abs(union.minX - screenFrame.minX) <= 32
- let touchesRight = abs(union.maxX - screenFrame.maxX) <= 32
- let coversWidth = union.width >= screenFrame.width * 0.92
- let coversHeight = union.height >= screenFrame.height * 0.86
- if touchesLeft && touchesRight && coversWidth && coversHeight {
- return true
- }
- }
- }
-
- return false
- }
-
- 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() {
@@ -1050,60 +915,8 @@
}
}
- private func makeOverlayWindow(on screen: NSScreen, initialQuickSearchSource: String? = nil) -> NSWindow {
- let panel = OverlayPanel(
- contentRect: screen.frame,
- styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
- backing: .buffered,
- defer: false
- )
- panel.isFloatingPanel = true
- panel.hidesOnDeactivate = false
- // Keep the overlay publishable above fullscreen Spaces without asking
- // AppKit to migrate an all-spaces window between Spaces.
- panel.collectionBehavior = [
- .canJoinAllSpaces,
- .fullScreenAuxiliary,
- .stationary,
- .transient,
- .ignoresCycle
- ]
- panel.isOpaque = false
- panel.backgroundColor = NSColor.black.withAlphaComponent(0.001)
- panel.hasShadow = false
- panel.titlebarAppearsTransparent = true
- panel.titleVisibility = .hidden
- panel.isReleasedWhenClosed = false
- panel.contentView = DismissibleHostingView(
- rootView: ContentView(
- hideOverlay: { [weak self] in
- self?.hideOverlay(force: true)
- },
- initialQuickSearchSource: initialQuickSearchSource
- ),
- onBackdropTap: { [weak self] in
- self?.hideOverlay()
- }
- )
- return panel
- }
-
private func hideOverlay(force: Bool = false, discardWindow: Bool = false) {
- guard force || !isInEditMode else { return }
- overlayGeneration &+= 1
- TagDatabase.flushPendingCategorySchemeBackupBatch()
- if let settingsWindow, settingsWindow.parent == overlayWindow {
- detachSettingsWindow(settingsWindow)
- }
- overlayWindow?.orderOut(nil)
- removeOverlayKeyMonitor()
- removeQuickSearchExternalMouseMonitor()
- overlayAvoidsSpaceSwitch = false
- refreshLauncherChromeState()
- NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
- if discardWindow {
- overlayWindow = nil
- }
+ overlayController.hide(force: force, discardWindow: discardWindow)
}
// MARK: - Global Hotkeys
@@ -1396,24 +1209,7 @@
}
private func screenUnderMouse() -> NSScreen? {
- overlayPlacementContextForNextOverlay(preferredScreen: nil)?.screen
- }
-
- private func overlayPlacementContextForNextOverlay(preferredScreen: NSScreen?) -> OverlayPlacementContext? {
- if let screen = preferredScreen {
- return OverlayPlacementContext(screen: screen, frame: screen.frame)
- }
- guard let screen = screenContainingCurrentPointer() ?? NSScreen.main ?? NSScreen.screens.first else {
- return nil
- }
- return OverlayPlacementContext(screen: screen, frame: screen.frame)
- }
-
- private func screenContainingCurrentPointer() -> NSScreen? {
- let mousePoint = NSEvent.mouseLocation
- return NSScreen.screens.first(where: {
- NSMouseInRect(mousePoint, $0.frame, false)
- })
+ overlayController.screenUnderMouse()
}
/// Clean up settingsWindow reference when the Settings window closes.
--
Gitblit v1.9.3