fix fullscreen
This commit is contained in:
@@ -7,139 +7,45 @@
|
|||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreGraphics
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import MacroVisionKit
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class FullscreenDetectionService: ObservableObject {
|
final class FullscreenDetectionService: ObservableObject {
|
||||||
@Published private(set) var isFullscreenActive = false
|
@Published private(set) var isFullscreenActive = false
|
||||||
|
|
||||||
private var observers: [NSObjectProtocol] = []
|
private var fullscreenTask: Task<Void, Never>?
|
||||||
private var frontmostAppObserver: AnyCancellable?
|
|
||||||
private let permissionManager: ScreenCapturePermissionManaging
|
private let permissionManager: ScreenCapturePermissionManaging
|
||||||
private let environmentProvider: FullscreenEnvironmentProviding
|
#if canImport(MacroVisionKit)
|
||||||
private let windowMatcher = FullscreenWindowMatcher()
|
private let monitor = FullScreenMonitor.shared
|
||||||
|
#endif
|
||||||
|
|
||||||
init(
|
init(
|
||||||
permissionManager: ScreenCapturePermissionManaging,
|
permissionManager: ScreenCapturePermissionManaging
|
||||||
environmentProvider: FullscreenEnvironmentProviding
|
|
||||||
) {
|
) {
|
||||||
self.permissionManager = permissionManager
|
self.permissionManager = permissionManager
|
||||||
self.environmentProvider = environmentProvider
|
startMonitoring()
|
||||||
setupObservers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience initializer using default services
|
/// Convenience initializer using default services
|
||||||
convenience init() {
|
convenience init() {
|
||||||
self.init(
|
self.init(
|
||||||
permissionManager: ScreenCapturePermissionManager.shared,
|
permissionManager: ScreenCapturePermissionManager.shared
|
||||||
environmentProvider: SystemFullscreenEnvironmentProvider()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factory method to safely create instances from non-main actor contexts
|
// Factory method to safely create instances from non-main actor contexts
|
||||||
static func create(
|
static func create(
|
||||||
permissionManager: ScreenCapturePermissionManaging? = nil,
|
permissionManager: ScreenCapturePermissionManaging? = nil
|
||||||
environmentProvider: FullscreenEnvironmentProviding? = nil
|
|
||||||
) async -> FullscreenDetectionService {
|
) async -> FullscreenDetectionService {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
return FullscreenDetectionService(
|
return FullscreenDetectionService(
|
||||||
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared,
|
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared
|
||||||
environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
let notificationCenter = NSWorkspace.shared.notificationCenter
|
fullscreenTask?.cancel()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canReadWindowInfo() -> Bool {
|
private func canReadWindowInfo() -> Bool {
|
||||||
@@ -151,25 +57,17 @@ final class FullscreenDetectionService: ObservableObject {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkFullscreenState() {
|
private func startMonitoring() {
|
||||||
guard canReadWindowInfo() else { return }
|
fullscreenTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
guard let frontmostPID = environmentProvider.frontmostProcessIdentifier() else {
|
let stream = await monitor.spaceChanges()
|
||||||
setFullscreenState(false)
|
for await spaces in stream {
|
||||||
return
|
guard self.canReadWindowInfo() else { continue }
|
||||||
}
|
self.setFullscreenState(!spaces.isEmpty)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFullscreenState(false)
|
forceUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func setFullscreenState(_ isActive: Bool) {
|
fileprivate func setFullscreenState(_ isActive: Bool) {
|
||||||
@@ -179,7 +77,12 @@ final class FullscreenDetectionService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func forceUpdate() {
|
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
|
#if DEBUG
|
||||||
@@ -188,16 +91,3 @@ final class FullscreenDetectionService: ObservableObject {
|
|||||||
}
|
}
|
||||||
#endif
|
#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