import AppKit
|
import CoreGraphics
|
import AlignerCore
|
|
@MainActor
|
final class QuickSwitchOverlayHost: OverlayHostProtocol {
|
private let panel: QuickSwitchPanel
|
private let screenProvider: (OverlayContext) -> NSScreen?
|
private let debugOverlayWidth: CGFloat?
|
private var currentScreen: NSScreen?
|
|
init(
|
screenProvider: ((OverlayContext) -> NSScreen?)? = nil,
|
title: String = "Aligner Quick Switch Overlay",
|
contentView: NSView? = nil,
|
debugOverlayWidth: CGFloat? = nil
|
) {
|
self.screenProvider = screenProvider ?? Self.screen(for:)
|
self.debugOverlayWidth = debugOverlayWidth
|
self.panel = QuickSwitchPanel()
|
self.panel.title = title
|
self.panel.contentView = contentView
|
currentScreen = self.screenProvider(OverlayContext(entryPoint: .quickSwitch))
|
applyWindowPolicy()
|
}
|
|
var currentLevel: CGWindowLevel {
|
CGWindowLevel(panel.level.rawValue)
|
}
|
|
func show(context: OverlayContext) {
|
currentScreen = screenProvider(context)
|
applyWindowPolicy()
|
panel.orderFrontRegardless()
|
}
|
|
func hide(reason: DismissReason) {
|
detachAllChildren()
|
panel.orderOut(nil)
|
}
|
|
func promoteForTextInput(allowAppActivation: Bool) {
|
panel.makeKeyAndOrderFront(nil)
|
if allowAppActivation {
|
NSApp.activate(ignoringOtherApps: true)
|
}
|
}
|
|
func attachChild(_ window: NSWindow) {
|
if let panel = window as? NSPanel {
|
OverlayWindowPolicy.apply(to: panel, on: currentScreen)
|
} else {
|
window.level = panel.level
|
window.collectionBehavior = panel.collectionBehavior
|
if let currentScreen {
|
window.setFrame(currentScreen.frame, display: false)
|
}
|
}
|
panel.addChildWindow(window, ordered: .above)
|
}
|
|
func detachChild(_ window: NSWindow) {
|
panel.removeChildWindow(window)
|
}
|
|
func beginSheet(_ sheetPanel: NSPanel, completion: (() -> Void)?) {
|
panel.beginSheet(sheetPanel) { _ in
|
completion?()
|
}
|
}
|
|
private func detachAllChildren() {
|
for child in panel.childWindows ?? [] {
|
panel.removeChildWindow(child)
|
child.orderOut(nil)
|
}
|
}
|
|
private func applyWindowPolicy() {
|
OverlayWindowPolicy.apply(to: panel, on: currentScreen)
|
|
guard
|
let currentScreen,
|
let debugOverlayWidth,
|
debugOverlayWidth > 0
|
else {
|
return
|
}
|
|
let width = min(debugOverlayWidth, currentScreen.frame.width)
|
let frame = CGRect(
|
x: currentScreen.frame.midX - width / 2,
|
y: currentScreen.frame.minY,
|
width: width,
|
height: currentScreen.frame.height
|
)
|
panel.setFrame(frame, display: false)
|
}
|
|
private static func screen(for context: OverlayContext) -> NSScreen? {
|
screen(matching: context.targetDisplayUUID) ?? NSScreen.main
|
}
|
|
private static func screen(matching displayUUID: String?) -> NSScreen? {
|
guard let displayUUID else { return nil }
|
|
return NSScreen.screens.first { screen in
|
guard
|
let number = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber,
|
let unmanagedUUID = CGDisplayCreateUUIDFromDisplayID(CGDirectDisplayID(number.uint32Value))
|
else {
|
return false
|
}
|
|
let uuid = unmanagedUUID.takeRetainedValue()
|
let screenUUID = CFUUIDCreateString(nil, uuid) as String?
|
return screenUUID?.caseInsensitiveCompare(displayUUID) == .orderedSame
|
}
|
}
|
}
|
|
@MainActor
|
private final class QuickSwitchPanel: NSPanel {
|
init() {
|
super.init(
|
contentRect: .zero,
|
styleMask: OverlayWindowPolicy.styleMask,
|
backing: .buffered,
|
defer: false
|
)
|
}
|
|
override var canBecomeKey: Bool {
|
true
|
}
|
|
override var canBecomeMain: Bool {
|
true
|
}
|
}
|