fix fullscreen
This commit is contained in:
@@ -7,139 +7,45 @@
|
||||
|
||||
import AppKit
|
||||
import Combine
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
public struct FullscreenWindowDescriptor: Equatable {
|
||||
public let ownerPID: pid_t
|
||||
public let layer: Int
|
||||
public let bounds: CGRect
|
||||
|
||||
public init(ownerPID: pid_t, layer: Int, bounds: CGRect) {
|
||||
self.ownerPID = ownerPID
|
||||
self.layer = layer
|
||||
self.bounds = bounds
|
||||
}
|
||||
}
|
||||
|
||||
protocol FullscreenEnvironmentProviding {
|
||||
func frontmostProcessIdentifier() -> pid_t?
|
||||
func windowDescriptors() -> [FullscreenWindowDescriptor]
|
||||
func screenFrames() -> [CGRect]
|
||||
}
|
||||
|
||||
struct SystemFullscreenEnvironmentProvider: FullscreenEnvironmentProviding {
|
||||
func frontmostProcessIdentifier() -> pid_t? {
|
||||
NSWorkspace.shared.frontmostApplication?.processIdentifier
|
||||
}
|
||||
|
||||
func windowDescriptors() -> [FullscreenWindowDescriptor] {
|
||||
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
||||
guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {
|
||||
return []
|
||||
}
|
||||
|
||||
return windowList.compactMap { window in
|
||||
guard let ownerPID = window[kCGWindowOwnerPID as String] as? pid_t,
|
||||
let layer = window[kCGWindowLayer as String] as? Int,
|
||||
let boundsDict = window[kCGWindowBounds as String] as? [String: CGFloat] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let bounds = CGRect(
|
||||
x: boundsDict["X"] ?? 0,
|
||||
y: boundsDict["Y"] ?? 0,
|
||||
width: boundsDict["Width"] ?? 0,
|
||||
height: boundsDict["Height"] ?? 0
|
||||
)
|
||||
|
||||
return FullscreenWindowDescriptor(ownerPID: ownerPID, layer: layer, bounds: bounds)
|
||||
}
|
||||
}
|
||||
|
||||
public func screenFrames() -> [CGRect] {
|
||||
NSScreen.screens.map(\.frame)
|
||||
}
|
||||
}
|
||||
import MacroVisionKit
|
||||
|
||||
final class FullscreenDetectionService: ObservableObject {
|
||||
@Published private(set) var isFullscreenActive = false
|
||||
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var frontmostAppObserver: AnyCancellable?
|
||||
private var fullscreenTask: Task<Void, Never>?
|
||||
private let permissionManager: ScreenCapturePermissionManaging
|
||||
private let environmentProvider: FullscreenEnvironmentProviding
|
||||
private let windowMatcher = FullscreenWindowMatcher()
|
||||
#if canImport(MacroVisionKit)
|
||||
private let monitor = FullScreenMonitor.shared
|
||||
#endif
|
||||
|
||||
init(
|
||||
permissionManager: ScreenCapturePermissionManaging,
|
||||
environmentProvider: FullscreenEnvironmentProviding
|
||||
permissionManager: ScreenCapturePermissionManaging
|
||||
) {
|
||||
self.permissionManager = permissionManager
|
||||
self.environmentProvider = environmentProvider
|
||||
setupObservers()
|
||||
startMonitoring()
|
||||
}
|
||||
|
||||
|
||||
/// Convenience initializer using default services
|
||||
convenience init() {
|
||||
self.init(
|
||||
permissionManager: ScreenCapturePermissionManager.shared,
|
||||
environmentProvider: SystemFullscreenEnvironmentProvider()
|
||||
permissionManager: ScreenCapturePermissionManager.shared
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Factory method to safely create instances from non-main actor contexts
|
||||
static func create(
|
||||
permissionManager: ScreenCapturePermissionManaging? = nil,
|
||||
environmentProvider: FullscreenEnvironmentProviding? = nil
|
||||
permissionManager: ScreenCapturePermissionManaging? = nil
|
||||
) async -> FullscreenDetectionService {
|
||||
await MainActor.run {
|
||||
return FullscreenDetectionService(
|
||||
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared,
|
||||
environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider()
|
||||
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
let notificationCenter = NSWorkspace.shared.notificationCenter
|
||||
observers.forEach { notificationCenter.removeObserver($0) }
|
||||
frontmostAppObserver?.cancel()
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
let workspace = NSWorkspace.shared
|
||||
let notificationCenter = workspace.notificationCenter
|
||||
|
||||
let stateChangeHandler: (Notification) -> Void = { [weak self] _ in
|
||||
self?.checkFullscreenState()
|
||||
}
|
||||
|
||||
let notifications: [(NSNotification.Name, Any?)] = [
|
||||
(NSWorkspace.activeSpaceDidChangeNotification, workspace),
|
||||
(NSApplication.didChangeScreenParametersNotification, nil),
|
||||
(NSWindow.willEnterFullScreenNotification, nil),
|
||||
(NSWindow.willExitFullScreenNotification, nil),
|
||||
]
|
||||
|
||||
observers = notifications.map { notification, object in
|
||||
notificationCenter.addObserver(
|
||||
forName: notification,
|
||||
object: object,
|
||||
queue: .main,
|
||||
using: stateChangeHandler
|
||||
)
|
||||
}
|
||||
|
||||
frontmostAppObserver = NotificationCenter.default.publisher(
|
||||
for: NSWorkspace.didActivateApplicationNotification,
|
||||
object: workspace
|
||||
)
|
||||
.sink { [weak self] _ in
|
||||
self?.checkFullscreenState()
|
||||
}
|
||||
|
||||
checkFullscreenState()
|
||||
fullscreenTask?.cancel()
|
||||
}
|
||||
|
||||
private func canReadWindowInfo() -> Bool {
|
||||
@@ -151,25 +57,17 @@ final class FullscreenDetectionService: ObservableObject {
|
||||
return true
|
||||
}
|
||||
|
||||
private func checkFullscreenState() {
|
||||
guard canReadWindowInfo() else { return }
|
||||
|
||||
guard let frontmostPID = environmentProvider.frontmostProcessIdentifier() else {
|
||||
setFullscreenState(false)
|
||||
return
|
||||
}
|
||||
|
||||
let windows = environmentProvider.windowDescriptors()
|
||||
let screens = environmentProvider.screenFrames()
|
||||
|
||||
for window in windows where window.ownerPID == frontmostPID && window.layer == 0 {
|
||||
if windowMatcher.isFullscreen(windowBounds: window.bounds, screenFrames: screens) {
|
||||
setFullscreenState(true)
|
||||
return
|
||||
private func startMonitoring() {
|
||||
fullscreenTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let stream = await monitor.spaceChanges()
|
||||
for await spaces in stream {
|
||||
guard self.canReadWindowInfo() else { continue }
|
||||
self.setFullscreenState(!spaces.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
setFullscreenState(false)
|
||||
forceUpdate()
|
||||
}
|
||||
|
||||
fileprivate func setFullscreenState(_ isActive: Bool) {
|
||||
@@ -179,7 +77,12 @@ final class FullscreenDetectionService: ObservableObject {
|
||||
}
|
||||
|
||||
func forceUpdate() {
|
||||
checkFullscreenState()
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
guard self.canReadWindowInfo() else { return }
|
||||
let spaces = await monitor.detectFullscreenApps()
|
||||
self.setFullscreenState(!spaces.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@@ -188,16 +91,3 @@ final class FullscreenDetectionService: ObservableObject {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
struct FullscreenWindowMatcher {
|
||||
func isFullscreen(windowBounds: CGRect, screenFrames: [CGRect], tolerance: CGFloat = 1) -> Bool {
|
||||
screenFrames.contains { matches(windowBounds, screenFrame: $0, tolerance: tolerance) }
|
||||
}
|
||||
|
||||
private func matches(_ windowBounds: CGRect, screenFrame: CGRect, tolerance: CGFloat) -> Bool {
|
||||
abs(windowBounds.width - screenFrame.width) < tolerance
|
||||
&& abs(windowBounds.height - screenFrame.height) < tolerance
|
||||
&& abs(windowBounds.origin.x - screenFrame.origin.x) < tolerance
|
||||
&& abs(windowBounds.origin.y - screenFrame.origin.y) < tolerance
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user