fix: fullscreen detection working
This commit is contained in:
@@ -20,25 +20,24 @@ enum EyeTrackingConstants {
|
|||||||
// MARK: - Face Pose Thresholds
|
// MARK: - Face Pose Thresholds
|
||||||
/// Maximum yaw (left/right head turn) in radians before considering user looking away
|
/// Maximum yaw (left/right head turn) in radians before considering user looking away
|
||||||
/// 0.20 radians ≈ 11.5 degrees (Tightened from 0.35)
|
/// 0.20 radians ≈ 11.5 degrees (Tightened from 0.35)
|
||||||
static let yawThreshold: Double = 0.1
|
static let yawThreshold: Double = 0.2
|
||||||
|
|
||||||
/// Pitch threshold for looking UP (above screen).
|
/// Pitch threshold for looking UP (above screen).
|
||||||
/// Since camera is at top, looking at screen is negative pitch.
|
/// Since camera is at top, looking at screen is negative pitch.
|
||||||
/// Values > 0.1 imply looking straight ahead or up (away from screen).
|
/// Values > 0.1 imply looking straight ahead or up (away from screen).
|
||||||
static let pitchUpThreshold: Double = 0.5
|
static let pitchUpThreshold: Double = 0.1
|
||||||
|
|
||||||
/// Pitch threshold for looking DOWN (at keyboard/lap).
|
/// Pitch threshold for looking DOWN (at keyboard/lap).
|
||||||
/// Values < -0.45 imply looking too far down.
|
/// Values < -0.45 imply looking too far down.
|
||||||
static let pitchDownThreshold: Double = -0.9
|
static let pitchDownThreshold: Double = -0.45
|
||||||
|
|
||||||
// MARK: - Pupil Tracking Thresholds
|
// MARK: - Pupil Tracking Thresholds
|
||||||
/// Minimum horizontal pupil ratio (0.0 = right edge, 1.0 = left edge)
|
/// Minimum horizontal pupil ratio (0.0 = right edge, 1.0 = left edge)
|
||||||
/// Values below this are considered looking right (camera view)
|
/// Values below this are considered looking right (camera view)
|
||||||
/// Tightened from 0.25 to 0.35
|
static let minPupilRatio: Double = 0.40
|
||||||
static let minPupilRatio: Double = 0.45
|
|
||||||
|
|
||||||
/// Maximum horizontal pupil ratio
|
/// Maximum horizontal pupil ratio
|
||||||
/// Values above this are considered looking left (camera view)
|
/// Values above this are considered looking left (camera view)
|
||||||
/// Tightened from 0.75 to 0.65
|
/// Tightened from 0.75 to 0.65
|
||||||
static let maxPupilRatio: Double = 0.55
|
static let maxPupilRatio: Double = 0.6
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,96 +7,182 @@
|
|||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import CoreGraphics
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
struct FullscreenWindowDescriptor: Equatable {
|
||||||
|
let ownerPID: pid_t
|
||||||
|
let layer: Int
|
||||||
|
let bounds: CGRect
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol FullscreenEnvironmentProviding {
|
||||||
|
func frontmostProcessIdentifier() -> pid_t?
|
||||||
|
func windowDescriptors() -> [FullscreenWindowDescriptor]
|
||||||
|
func screenFrames() -> [CGRect]
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func screenFrames() -> [CGRect] {
|
||||||
|
NSScreen.screens.map(\.frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class FullscreenDetectionService: ObservableObject {
|
class FullscreenDetectionService: ObservableObject {
|
||||||
@Published private(set) var isFullscreenActive = false
|
@Published private(set) var isFullscreenActive = false
|
||||||
|
|
||||||
private var observers: [NSObjectProtocol] = []
|
private var observers: [NSObjectProtocol] = []
|
||||||
|
private var frontmostAppObserver: AnyCancellable?
|
||||||
|
private let permissionManager: ScreenCapturePermissionManaging
|
||||||
|
private let environmentProvider: FullscreenEnvironmentProviding
|
||||||
|
|
||||||
init() {
|
init(
|
||||||
|
permissionManager: ScreenCapturePermissionManaging? = nil,
|
||||||
|
environmentProvider: FullscreenEnvironmentProviding? = nil
|
||||||
|
) {
|
||||||
|
self.permissionManager = permissionManager ?? ScreenCapturePermissionManager.shared
|
||||||
|
self.environmentProvider = environmentProvider ?? SystemFullscreenEnvironmentProvider()
|
||||||
setupObservers()
|
setupObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
let notificationCenter = NSWorkspace.shared.notificationCenter
|
let notificationCenter = NSWorkspace.shared.notificationCenter
|
||||||
observers.forEach { notificationCenter.removeObserver($0) }
|
observers.forEach { notificationCenter.removeObserver($0) }
|
||||||
|
frontmostAppObserver?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupObservers() {
|
private func setupObservers() {
|
||||||
let workspace = NSWorkspace.shared
|
let workspace = NSWorkspace.shared
|
||||||
let notificationCenter = workspace.notificationCenter
|
let notificationCenter = workspace.notificationCenter
|
||||||
|
|
||||||
// Monitor when applications enter fullscreen
|
let spaceObserver = notificationCenter.addObserver(
|
||||||
let didEnterObserver = notificationCenter.addObserver(
|
|
||||||
forName: NSWorkspace.activeSpaceDidChangeNotification,
|
forName: NSWorkspace.activeSpaceDidChangeNotification,
|
||||||
object: workspace,
|
object: workspace,
|
||||||
queue: .main
|
queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
Task { @MainActor in
|
|
||||||
self?.checkFullscreenState()
|
self?.checkFullscreenState()
|
||||||
}
|
}
|
||||||
}
|
observers.append(spaceObserver)
|
||||||
observers.append(didEnterObserver)
|
|
||||||
|
|
||||||
// Monitor when active application changes
|
let transitionObserver = notificationCenter.addObserver(
|
||||||
let didActivateObserver = notificationCenter.addObserver(
|
forName: NSApplication.didChangeScreenParametersNotification,
|
||||||
forName: NSWorkspace.didActivateApplicationNotification,
|
object: nil,
|
||||||
object: workspace,
|
|
||||||
queue: .main
|
queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
Task { @MainActor in
|
|
||||||
self?.checkFullscreenState()
|
self?.checkFullscreenState()
|
||||||
}
|
}
|
||||||
}
|
observers.append(transitionObserver)
|
||||||
observers.append(didActivateObserver)
|
|
||||||
|
let fullscreenObserver = notificationCenter.addObserver(
|
||||||
|
forName: NSWindow.willEnterFullScreenNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.checkFullscreenState()
|
||||||
|
}
|
||||||
|
observers.append(fullscreenObserver)
|
||||||
|
|
||||||
|
let exitFullscreenObserver = notificationCenter.addObserver(
|
||||||
|
forName: NSWindow.willExitFullScreenNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.checkFullscreenState()
|
||||||
|
}
|
||||||
|
observers.append(exitFullscreenObserver)
|
||||||
|
|
||||||
|
frontmostAppObserver = NotificationCenter.default.publisher(
|
||||||
|
for: NSWorkspace.didActivateApplicationNotification,
|
||||||
|
object: workspace
|
||||||
|
)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.checkFullscreenState()
|
||||||
|
}
|
||||||
|
|
||||||
// Initial check
|
|
||||||
checkFullscreenState()
|
checkFullscreenState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canReadWindowInfo() -> Bool {
|
||||||
|
guard permissionManager.authorizationStatus.isAuthorized else {
|
||||||
|
setFullscreenState(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private func checkFullscreenState() {
|
private func checkFullscreenState() {
|
||||||
guard let frontmostApp = NSWorkspace.shared.frontmostApplication else {
|
guard canReadWindowInfo() else { return }
|
||||||
isFullscreenActive = false
|
|
||||||
|
guard let frontmostPID = environmentProvider.frontmostProcessIdentifier() else {
|
||||||
|
setFullscreenState(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any window of the frontmost application is fullscreen
|
let windows = environmentProvider.windowDescriptors()
|
||||||
let options = CGWindowListOption(arrayLiteral: .optionOnScreenOnly, .excludeDesktopElements)
|
let screens = environmentProvider.screenFrames()
|
||||||
let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? []
|
|
||||||
|
|
||||||
let frontmostPID = frontmostApp.processIdentifier
|
for window in windows where window.ownerPID == frontmostPID && window.layer == 0 {
|
||||||
|
if screens.contains(where: { FullscreenDetectionService.window(window.bounds, matches: $0) }) {
|
||||||
for window in windowList {
|
setFullscreenState(true)
|
||||||
guard let ownerPID = window[kCGWindowOwnerPID as String] as? pid_t,
|
|
||||||
ownerPID == frontmostPID,
|
|
||||||
let bounds = window[kCGWindowBounds as String] as? [String: CGFloat],
|
|
||||||
let layer = window[kCGWindowLayer as String] as? Int else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if window is fullscreen by comparing bounds to screen size
|
|
||||||
if let screen = NSScreen.main {
|
|
||||||
let screenFrame = screen.frame
|
|
||||||
let windowWidth = bounds["Width"] ?? 0
|
|
||||||
let windowHeight = bounds["Height"] ?? 0
|
|
||||||
|
|
||||||
// Window is considered fullscreen if it matches screen dimensions
|
|
||||||
// and is at a normal window layer (0)
|
|
||||||
if layer == 0 &&
|
|
||||||
abs(windowWidth - screenFrame.width) < 1 &&
|
|
||||||
abs(windowHeight - screenFrame.height) < 1 {
|
|
||||||
isFullscreenActive = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFullscreenState(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
isFullscreenActive = false
|
private static func window(_ windowBounds: CGRect, matches screenFrame: CGRect, tolerance: CGFloat = 1) -> 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
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func setFullscreenState(_ isActive: Bool) {
|
||||||
|
guard isFullscreenActive != isActive else { return }
|
||||||
|
isFullscreenActive = isActive
|
||||||
|
print("🖥️ Fullscreen state updated: \(isActive ? "ACTIVE" : "INACTIVE")")
|
||||||
}
|
}
|
||||||
|
|
||||||
func forceUpdate() {
|
func forceUpdate() {
|
||||||
checkFullscreenState()
|
checkFullscreenState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
func simulateFullscreenStateForTesting(_ isActive: Bool) {
|
||||||
|
setFullscreenState(isActive)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// ScreenCapturePermissionManager.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by ChatGPT on 1/14/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
import Combine
|
||||||
|
import CoreGraphics
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum ScreenCaptureAuthorizationStatus: Equatable {
|
||||||
|
case authorized
|
||||||
|
case denied
|
||||||
|
case notDetermined
|
||||||
|
|
||||||
|
var isAuthorized: Bool {
|
||||||
|
if case .authorized = self { return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public protocol ScreenCapturePermissionManaging: AnyObject {
|
||||||
|
var authorizationStatus: ScreenCaptureAuthorizationStatus { get }
|
||||||
|
var authorizationStatusPublisher: AnyPublisher<ScreenCaptureAuthorizationStatus, Never> { get }
|
||||||
|
|
||||||
|
func refreshStatus()
|
||||||
|
func requestAuthorizationIfNeeded()
|
||||||
|
func openSystemSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class ScreenCapturePermissionManager: ObservableObject, ScreenCapturePermissionManaging {
|
||||||
|
static let shared = ScreenCapturePermissionManager()
|
||||||
|
|
||||||
|
@Published private(set) var authorizationStatus: ScreenCaptureAuthorizationStatus = .notDetermined
|
||||||
|
|
||||||
|
var authorizationStatusPublisher: AnyPublisher<ScreenCaptureAuthorizationStatus, Never> {
|
||||||
|
$authorizationStatus.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let userDefaults: UserDefaults
|
||||||
|
private let requestedKey = "gazeScreenCapturePermissionRequested"
|
||||||
|
|
||||||
|
init(userDefaults: UserDefaults = .standard) {
|
||||||
|
self.userDefaults = userDefaults
|
||||||
|
refreshStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshStatus() {
|
||||||
|
if CGPreflightScreenCaptureAccess() {
|
||||||
|
authorizationStatus = .authorized
|
||||||
|
} else if userDefaults.bool(forKey: requestedKey) {
|
||||||
|
authorizationStatus = .denied
|
||||||
|
} else {
|
||||||
|
authorizationStatus = .notDetermined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAuthorizationIfNeeded() {
|
||||||
|
refreshStatus()
|
||||||
|
|
||||||
|
guard authorizationStatus == .notDetermined else { return }
|
||||||
|
|
||||||
|
userDefaults.set(true, forKey: requestedKey)
|
||||||
|
let granted = CGRequestScreenCaptureAccess()
|
||||||
|
authorizationStatus = granted ? .authorized : .denied
|
||||||
|
}
|
||||||
|
|
||||||
|
func openSystemSettings() {
|
||||||
|
guard let url = URL(
|
||||||
|
string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording"
|
||||||
|
) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SmartModeSetupView: View {
|
struct SmartModeSetupView: View {
|
||||||
@ObservedObject var settingsManager: SettingsManager
|
@ObservedObject var settingsManager: SettingsManager
|
||||||
|
@StateObject private var permissionManager = ScreenCapturePermissionManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -55,11 +56,51 @@ struct SmartModeSetupView: View {
|
|||||||
set: { newValue in
|
set: { newValue in
|
||||||
print("🔧 Smart Mode - Auto-pause on fullscreen changed to: \(newValue)")
|
print("🔧 Smart Mode - Auto-pause on fullscreen changed to: \(newValue)")
|
||||||
settingsManager.settings.smartMode.autoPauseOnFullscreen = newValue
|
settingsManager.settings.smartMode.autoPauseOnFullscreen = newValue
|
||||||
|
|
||||||
|
if newValue {
|
||||||
|
permissionManager.requestAuthorizationIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settingsManager.settings.smartMode.autoPauseOnFullscreen,
|
||||||
|
permissionManager.authorizationStatus != .authorized
|
||||||
|
{
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Label(
|
||||||
|
permissionManager.authorizationStatus == .denied
|
||||||
|
? "Screen Recording permission required"
|
||||||
|
: "Grant Screen Recording access",
|
||||||
|
systemImage: "exclamationmark.shield"
|
||||||
|
)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"macOS requires Screen Recording permission to detect other apps in fullscreen."
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("Grant Access") {
|
||||||
|
permissionManager.requestAuthorizationIfNeeded()
|
||||||
|
permissionManager.openSystemSettings()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
|
Button("Open Settings") {
|
||||||
|
permissionManager.openSystemSettings()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8))
|
||||||
|
|||||||
120
GazeTests/FullscreenDetectionServiceTests.swift
Normal file
120
GazeTests/FullscreenDetectionServiceTests.swift
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
//
|
||||||
|
// FullscreenDetectionServiceTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by ChatGPT on 1/14/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreGraphics
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class FullscreenDetectionServiceTests: XCTestCase {
|
||||||
|
func testPermissionDeniedKeepsStateFalse() {
|
||||||
|
let mockManager = MockPermissionManager(status: ScreenCaptureAuthorizationStatus.denied)
|
||||||
|
let service = FullscreenDetectionService(permissionManager: mockManager)
|
||||||
|
|
||||||
|
let expectation = expectation(description: "No change")
|
||||||
|
expectation.isInverted = true
|
||||||
|
|
||||||
|
let cancellable = service.$isFullscreenActive
|
||||||
|
.dropFirst()
|
||||||
|
.sink { _ in
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
service.forceUpdate()
|
||||||
|
|
||||||
|
wait(for: [expectation], timeout: 0.5)
|
||||||
|
cancellable.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFullscreenStateBecomesTrueWhenWindowMatchesScreen() {
|
||||||
|
let mockManager = MockPermissionManager(status: ScreenCaptureAuthorizationStatus.authorized)
|
||||||
|
let environment = MockFullscreenEnvironment(
|
||||||
|
frontmostPID: 42,
|
||||||
|
windowDescriptors: [
|
||||||
|
FullscreenWindowDescriptor(
|
||||||
|
ownerPID: 42,
|
||||||
|
layer: 0,
|
||||||
|
bounds: CGRect(x: 0, y: 0, width: 1920, height: 1080)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
screenFrames: [CGRect(x: 0, y: 0, width: 1920, height: 1080)]
|
||||||
|
)
|
||||||
|
|
||||||
|
let service = FullscreenDetectionService(
|
||||||
|
permissionManager: mockManager,
|
||||||
|
environmentProvider: environment
|
||||||
|
)
|
||||||
|
|
||||||
|
let expectation = expectation(description: "Fullscreen detected")
|
||||||
|
|
||||||
|
let cancellable = service.$isFullscreenActive
|
||||||
|
.dropFirst()
|
||||||
|
.sink { isActive in
|
||||||
|
if isActive {
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.forceUpdate()
|
||||||
|
|
||||||
|
wait(for: [expectation], timeout: 0.5)
|
||||||
|
cancellable.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFullscreenStateStaysFalseWhenWindowDoesNotMatchScreen() {
|
||||||
|
let mockManager = MockPermissionManager(status: ScreenCaptureAuthorizationStatus.authorized)
|
||||||
|
let environment = MockFullscreenEnvironment(
|
||||||
|
frontmostPID: 42,
|
||||||
|
windowDescriptors: [
|
||||||
|
FullscreenWindowDescriptor(
|
||||||
|
ownerPID: 42,
|
||||||
|
layer: 0,
|
||||||
|
bounds: CGRect(x: 100, y: 100, width: 800, height: 600)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
screenFrames: [CGRect(x: 0, y: 0, width: 1920, height: 1080)]
|
||||||
|
)
|
||||||
|
|
||||||
|
let service = FullscreenDetectionService(
|
||||||
|
permissionManager: mockManager,
|
||||||
|
environmentProvider: environment
|
||||||
|
)
|
||||||
|
|
||||||
|
let expectation = expectation(description: "No fullscreen")
|
||||||
|
expectation.isInverted = true
|
||||||
|
|
||||||
|
let cancellable = service.$isFullscreenActive
|
||||||
|
.dropFirst()
|
||||||
|
.sink { isActive in
|
||||||
|
if isActive {
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.forceUpdate()
|
||||||
|
|
||||||
|
wait(for: [expectation], timeout: 0.5)
|
||||||
|
cancellable.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class MockPermissionManager: ScreenCapturePermissionManaging {
|
||||||
|
var authorizationStatus: ScreenCaptureAuthorizationStatus
|
||||||
|
var authorizationStatusPublisher: AnyPublisher<ScreenCaptureAuthorizationStatus, Never> {
|
||||||
|
Just(authorizationStatus).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(status: ScreenCaptureAuthorizationStatus) {
|
||||||
|
self.authorizationStatus = status
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshStatus() {}
|
||||||
|
func requestAuthorizationIfNeeded() {}
|
||||||
|
func openSystemSettings() {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user