MonitorControl/MonitorControl/Support/DisplayManager.swift
2024-10-02 13:46:54 +02:00

529 lines
22 KiB
Swift

// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
import Cocoa
import CoreGraphics
import os.log
class DisplayManager {
public static let shared = DisplayManager()
var displays: [Display] = []
var audioControlTargetDisplays: [OtherDisplay] = []
let globalDDCQueue = DispatchQueue(label: "Global DDC queue")
let gammaActivityEnforcer = NSWindow(contentRect: .init(origin: NSPoint(x: 0, y: 0), size: .init(width: DEBUG_GAMMA_ENFORCER ? 15 : 1, height: DEBUG_GAMMA_ENFORCER ? 15 : 1)), styleMask: [], backing: .buffered, defer: false)
var gammaInterferenceCounter = 0
var gammaInterferenceWarningShown = false
func createGammaActivityEnforcer() {
self.gammaActivityEnforcer.title = "Monitor Control Gamma Activity Enforcer"
self.gammaActivityEnforcer.isMovableByWindowBackground = false
self.gammaActivityEnforcer.backgroundColor = DEBUG_GAMMA_ENFORCER ? .red : .black
self.gammaActivityEnforcer.ignoresMouseEvents = true
self.gammaActivityEnforcer.level = .screenSaver
self.gammaActivityEnforcer.orderFrontRegardless()
self.gammaActivityEnforcer.collectionBehavior = [.stationary, .canJoinAllSpaces]
os_log("Gamma activity enforcer created.", type: .info)
}
func enforceGammaActivity() {
if self.gammaActivityEnforcer.alphaValue == 1 * (DEBUG_GAMMA_ENFORCER ? 0.5 : 0.01) {
self.gammaActivityEnforcer.alphaValue = 2 * (DEBUG_GAMMA_ENFORCER ? 0.5 : 0.01)
} else {
self.gammaActivityEnforcer.alphaValue = 1 * (DEBUG_GAMMA_ENFORCER ? 0.5 : 0.01)
}
}
func moveGammaActivityEnforcer(displayID: CGDirectDisplayID) {
if let screen = DisplayManager.getByDisplayID(displayID: DisplayManager.resolveEffectiveDisplayID(displayID)) {
self.gammaActivityEnforcer.setFrameOrigin(screen.frame.origin)
}
self.gammaActivityEnforcer.orderFrontRegardless()
}
var shades: [CGDirectDisplayID: NSWindow] = [:]
var shadeGrave: [NSWindow] = []
func isDisqualifiedFromShade(_ displayID: CGDirectDisplayID) -> Bool {
if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 {
if displayID == DisplayManager.resolveEffectiveDisplayID(displayID), DisplayManager.isVirtual(displayID: displayID) || DisplayManager.isDummy(displayID: displayID) {
var displayIDs = [CGDirectDisplayID](repeating: 0, count: 16)
var displayCount: UInt32 = 0
guard CGGetOnlineDisplayList(16, &displayIDs, &displayCount) == .success else {
return true
}
for displayId in displayIDs where CGDisplayMirrorsDisplay(displayId) == displayID && !DisplayManager.isVirtual(displayID: displayID) {
return true
}
return false
}
return true
}
return false
}
func createShadeOnDisplay(displayID: CGDirectDisplayID) -> NSWindow? {
if let screen = DisplayManager.getByDisplayID(displayID: displayID) {
let shade = NSWindow(contentRect: .init(origin: NSPoint(x: 0, y: 0), size: .init(width: 10, height: 1)), styleMask: [], backing: .buffered, defer: false)
shade.title = "Monitor Control Window Shade for Display " + String(displayID)
shade.isMovableByWindowBackground = false
shade.backgroundColor = .clear
shade.ignoresMouseEvents = true
shade.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
shade.orderFrontRegardless()
shade.collectionBehavior = [.stationary, .canJoinAllSpaces, .ignoresCycle]
shade.setFrame(screen.frame, display: true)
shade.contentView?.wantsLayer = true
shade.contentView?.alphaValue = 0.0
shade.contentView?.layer?.backgroundColor = .black
shade.contentView?.setNeedsDisplay(shade.frame)
os_log("Window shade created for display %{public}@", type: .info, String(displayID))
return shade
}
return nil
}
func getShade(displayID: CGDirectDisplayID) -> NSWindow? {
guard !self.isDisqualifiedFromShade(displayID) else {
return nil
}
if let shade = shades[displayID] {
return shade
} else {
if let shade = self.createShadeOnDisplay(displayID: displayID) {
self.shades[displayID] = shade
return shade
}
}
return nil
}
func destroyAllShades() -> Bool {
var ret = false
for displayID in self.shades.keys {
os_log("Attempting to destory shade for display %{public}@", type: .info, String(displayID))
if self.destroyShade(displayID: displayID) {
ret = true
}
}
if ret {
os_log("Destroyed all shades.", type: .info)
} else {
os_log("No shades were found to be destroyed.", type: .info)
}
return ret
}
func destroyShade(displayID: CGDirectDisplayID) -> Bool {
if let shade = shades[displayID] {
os_log("Destroying shade for display %{public}@", type: .info, String(displayID))
self.shadeGrave.append(shade)
self.shades.removeValue(forKey: displayID)
shade.close()
return true
}
return false
}
func updateShade(displayID: CGDirectDisplayID) -> Bool {
guard !self.isDisqualifiedFromShade(displayID) else {
return false
}
if let screen = DisplayManager.getByDisplayID(displayID: displayID) {
if let shade = getShade(displayID: displayID) {
shade.setFrame(screen.frame, display: true)
return true
}
}
return false
}
func getShadeAlpha(displayID: CGDirectDisplayID) -> Float? {
guard !self.isDisqualifiedFromShade(displayID) else {
return 1
}
if let shade = getShade(displayID: displayID) {
return Float(shade.contentView?.alphaValue ?? 1)
} else {
return 1
}
}
func setShadeAlpha(value: Float, displayID: CGDirectDisplayID) -> Bool {
guard !self.isDisqualifiedFromShade(displayID) else {
return false
}
if let shade = getShade(displayID: displayID) {
shade.contentView?.alphaValue = CGFloat(value)
return true
}
return false
}
func configureDisplays() {
self.clearDisplays()
var onlineDisplayIDs = [CGDirectDisplayID](repeating: 0, count: 16)
var displayCount: UInt32 = 0
guard CGGetOnlineDisplayList(16, &onlineDisplayIDs, &displayCount) == .success else {
os_log("Unable to get display list.", type: .info)
return
}
for onlineDisplayID in onlineDisplayIDs where onlineDisplayID != 0 {
let name = DisplayManager.getDisplayNameByID(displayID: onlineDisplayID)
let id = onlineDisplayID
let vendorNumber = CGDisplayVendorNumber(onlineDisplayID)
let modelNumber = CGDisplayModelNumber(onlineDisplayID)
let serialNumber = CGDisplaySerialNumber(onlineDisplayID)
let isDummy: Bool = DisplayManager.isDummy(displayID: onlineDisplayID)
let isVirtual: Bool = DisplayManager.isVirtual(displayID: onlineDisplayID)
if !DEBUG_SW, DisplayManager.isAppleDisplay(displayID: onlineDisplayID) { // MARK: (point of interest for testing)
let appleDisplay = AppleDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, serialNumber: serialNumber, isVirtual: isVirtual, isDummy: isDummy)
os_log("Apple display found - %{public}@", type: .info, "ID: \(appleDisplay.identifier), Name: \(appleDisplay.name) (Vendor: \(appleDisplay.vendorNumber ?? 0), Model: \(appleDisplay.modelNumber ?? 0))")
self.addDisplay(display: appleDisplay)
} else {
let otherDisplay = OtherDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, serialNumber: serialNumber, isVirtual: isVirtual, isDummy: isDummy)
os_log("Other display found - %{public}@", type: .info, "ID: \(otherDisplay.identifier), Name: \(otherDisplay.name) (Vendor: \(otherDisplay.vendorNumber ?? 0), Model: \(otherDisplay.modelNumber ?? 0))")
self.addDisplay(display: otherDisplay)
}
}
}
func setupOtherDisplays(firstrun: Bool = false) {
for otherDisplay in self.getOtherDisplays() {
for command in [Command.audioSpeakerVolume, Command.contrast] where !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: command) && !otherDisplay.isSw() {
otherDisplay.setupCurrentAndMaxValues(command: command, firstrun: firstrun)
}
if (!otherDisplay.isSw() && !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: .brightness)) || otherDisplay.isSw() {
otherDisplay.setupCurrentAndMaxValues(command: .brightness, firstrun: firstrun)
otherDisplay.brightnessSyncSourceValue = otherDisplay.readPrefAsFloat(for: .brightness)
}
}
}
func restoreOtherDisplays() {
for otherDisplay in self.getDdcCapableDisplays() {
for command in [Command.contrast, Command.brightness] where !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: command) {
otherDisplay.restoreDDCSettingsToDisplay(command: command)
}
}
}
func normalizedName(_ name: String) -> String {
var normalizedName = name.replacingOccurrences(of: "(", with: "")
normalizedName = normalizedName.replacingOccurrences(of: ")", with: "")
normalizedName = normalizedName.replacingOccurrences(of: " ", with: "")
for i in 0 ... 9 {
normalizedName = normalizedName.replacingOccurrences(of: String(i), with: "")
}
return normalizedName
}
func updateAudioControlTargetDisplays(deviceName: String) -> Int {
self.audioControlTargetDisplays.removeAll()
os_log("Detecting displays for audio control via audio device name matching...", type: .info)
var numOfAddedDisplays = 0
for ddcCapableDisplay in self.getDdcCapableDisplays() {
var displayAudioDeviceName = ddcCapableDisplay.readPrefAsString(key: .audioDeviceNameOverride)
if displayAudioDeviceName == "" {
displayAudioDeviceName = DisplayManager.getDisplayRawNameByID(displayID: ddcCapableDisplay.identifier)
}
if self.normalizedName(displayAudioDeviceName) == self.normalizedName(deviceName) {
self.audioControlTargetDisplays.append(ddcCapableDisplay)
numOfAddedDisplays += 1
os_log("Added display for audio control - %{public}@", type: .info, ddcCapableDisplay.name)
}
}
return numOfAddedDisplays
}
func getOtherDisplays() -> [OtherDisplay] {
self.displays.compactMap { $0 as? OtherDisplay }
}
func getAllDisplays() -> [Display] {
self.displays
}
func getDdcCapableDisplays() -> [OtherDisplay] {
self.displays.compactMap { display -> OtherDisplay? in
if let otherDisplay = display as? OtherDisplay, !otherDisplay.isSw() {
return otherDisplay
} else { return nil }
}
}
func getAppleDisplays() -> [AppleDisplay] {
self.displays.compactMap { $0 as? AppleDisplay }
}
func getBuiltInDisplay() -> Display? {
self.displays.first { CGDisplayIsBuiltin($0.identifier) != 0 }
}
func getCurrentDisplay(byFocus: Bool = false) -> Display? {
if byFocus {
guard let mainDisplayID = NSScreen.main?.displayID else {
return nil
}
return self.displays.first { $0.identifier == mainDisplayID }
} else {
let mouseLocation = NSEvent.mouseLocation
let screens = NSScreen.screens
if let screenWithMouse = (screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }) {
return self.displays.first { $0.identifier == screenWithMouse.displayID }
}
return nil
}
}
func addDisplay(display: Display) {
self.displays.append(display)
}
func clearDisplays() {
self.displays = []
}
func addDisplayCounterSuffixes() {
var nameDisplays: [String: [Display]] = [:]
for display in self.displays {
if nameDisplays[display.name] != nil {
nameDisplays[display.name]?.append(display)
} else {
nameDisplays[display.name] = [display]
}
}
for nameDisplayKey in nameDisplays.keys where nameDisplays[nameDisplayKey]?.count ?? 0 > 1 {
for i in 0 ... (nameDisplays[nameDisplayKey]?.count ?? 1) - 1 {
if let display = nameDisplays[nameDisplayKey]?[i] {
display.name = "" + display.name + " (" + String(i + 1) + ")"
}
}
}
}
func updateArm64AVServices() {
if Arm64DDC.isArm64 {
os_log("arm64 AVService update requested", type: .info)
var displayIDs: [CGDirectDisplayID] = []
for otherDisplay in self.getOtherDisplays() {
displayIDs.append(otherDisplay.identifier)
}
for serviceMatch in Arm64DDC.getServiceMatches(displayIDs: displayIDs) {
for otherDisplay in self.getOtherDisplays() where otherDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil {
otherDisplay.arm64avService = serviceMatch.service
os_log("Display service match successful for display %{public}@", type: .info, String(serviceMatch.displayID))
if serviceMatch.discouraged {
os_log("Display %{public}@ is flagged as discouraged by Arm64DDC.", type: .info, String(serviceMatch.displayID))
otherDisplay.isDiscouraged = true
} else if serviceMatch.dummy {
os_log("Display %{public}@ is flagged as dummy by Arm64DDC.", type: .info, String(serviceMatch.displayID))
otherDisplay.isDiscouraged = true
otherDisplay.isDummy = true
} else {
otherDisplay.arm64ddc = DEBUG_SW ? false : true // MARK: (point of interest when testing)
}
}
}
os_log("AVService update done", type: .info)
}
}
func resetSwBrightnessForAllDisplays(prefsOnly: Bool = false, noPrefSave: Bool = false, async: Bool = false) {
for otherDisplay in self.getOtherDisplays() {
if !prefsOnly {
_ = otherDisplay.setSwBrightness(1, smooth: async, noPrefSave: noPrefSave)
if !noPrefSave {
otherDisplay.smoothBrightnessTransient = 1
}
} else if !noPrefSave {
otherDisplay.savePref(1, key: .SwBrightness)
otherDisplay.smoothBrightnessTransient = 1
}
if otherDisplay.isSw(), !noPrefSave {
otherDisplay.savePref(1, for: .brightness)
}
}
}
func restoreSwBrightnessForAllDisplays(async: Bool = false) {
for otherDisplay in self.getOtherDisplays() {
if (otherDisplay.readPrefAsFloat(for: .brightness) == 0 && !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue)) || (otherDisplay.readPrefAsFloat(for: .brightness) < otherDisplay.combinedBrightnessSwitchingValue() && !prefs.bool(forKey: PrefKey.separateCombinedScale.rawValue) && !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue)) || otherDisplay.isSw() {
let savedPrefValue = otherDisplay.readPrefAsFloat(key: .SwBrightness)
if otherDisplay.getSwBrightness() != savedPrefValue {
OSDUtils.popEmptyOsd(displayID: otherDisplay.identifier, command: Command.brightness) // This will give the user a hint why is the brightness suddenly changes.
}
otherDisplay.savePref(otherDisplay.getSwBrightness(), key: .SwBrightness)
os_log("Restoring sw brightness to %{public}@ on other display %{public}@", type: .info, String(savedPrefValue), String(otherDisplay.identifier))
_ = otherDisplay.setSwBrightness(savedPrefValue, smooth: async)
if otherDisplay.isSw(), let slider = otherDisplay.sliderHandler[.brightness] {
os_log("Restoring sw slider to %{public}@ for other display %{public}@", type: .info, String(savedPrefValue), String(otherDisplay.identifier))
slider.setValue(savedPrefValue, displayID: otherDisplay.identifier)
}
} else {
_ = otherDisplay.setSwBrightness(1)
}
}
}
func getAffectedDisplays(isBrightness: Bool = false, isVolume: Bool = false) -> [Display]? {
var affectedDisplays: [Display]
let allDisplays = self.getAllDisplays()
var currentDisplay: Display?
if isBrightness {
if prefs.integer(forKey: PrefKey.multiKeyboardBrightness.rawValue) == MultiKeyboardBrightness.allScreens.rawValue {
affectedDisplays = allDisplays
return affectedDisplays
}
currentDisplay = self.getCurrentDisplay(byFocus: prefs.integer(forKey: PrefKey.multiKeyboardBrightness.rawValue) == MultiKeyboardBrightness.focusInsteadOfMouse.rawValue)
}
if isVolume {
if prefs.integer(forKey: PrefKey.multiKeyboardVolume.rawValue) == MultiKeyboardVolume.allScreens.rawValue {
affectedDisplays = allDisplays
return affectedDisplays
} else if prefs.integer(forKey: PrefKey.multiKeyboardVolume.rawValue) == MultiKeyboardVolume.audioDeviceNameMatching.rawValue {
return self.audioControlTargetDisplays
}
currentDisplay = self.getCurrentDisplay(byFocus: false)
}
if let currentDisplay = currentDisplay {
affectedDisplays = [currentDisplay]
if CGDisplayIsInHWMirrorSet(currentDisplay.identifier) != 0 || CGDisplayIsInMirrorSet(currentDisplay.identifier) != 0, CGDisplayMirrorsDisplay(currentDisplay.identifier) == 0 {
for display in allDisplays where CGDisplayMirrorsDisplay(display.identifier) == currentDisplay.identifier {
affectedDisplays.append(display)
}
}
} else {
affectedDisplays = []
}
return affectedDisplays
}
static func isDummy(displayID: CGDirectDisplayID) -> Bool {
let vendorNumber = CGDisplayVendorNumber(displayID)
let rawName = DisplayManager.getDisplayRawNameByID(displayID: displayID)
if rawName.lowercased().contains("dummy") || (self.isVirtual(displayID: displayID) && vendorNumber == UInt32(0xF0F0)) {
os_log("NOTE: Display is a dummy!", type: .info)
return true
}
return false
}
static func isVirtual(displayID: CGDirectDisplayID) -> Bool {
var isVirtual = false
if !DEBUG_MACOS10, #available(macOS 11.0, *) {
if let dictionary = (CoreDisplay_DisplayCreateInfoDictionary(displayID)?.takeRetainedValue() as NSDictionary?) {
let isVirtualDevice = dictionary["kCGDisplayIsVirtualDevice"] as? Bool
let displayIsAirplay = dictionary["kCGDisplayIsAirPlay"] as? Bool
if isVirtualDevice ?? displayIsAirplay ?? false {
isVirtual = true
}
}
}
return isVirtual
}
static func engageMirror() -> Bool {
var onlineDisplayIDs = [CGDirectDisplayID](repeating: 0, count: 16)
var displayCount: UInt32 = 0
guard CGGetOnlineDisplayList(16, &onlineDisplayIDs, &displayCount) == .success, displayCount > 1 else {
return false
}
// Break display mirror if there is any
var mirrorBreak = false
var displayConfigRef: CGDisplayConfigRef?
for onlineDisplayID in onlineDisplayIDs where onlineDisplayID != 0 {
if CGDisplayIsInHWMirrorSet(onlineDisplayID) != 0 || CGDisplayIsInMirrorSet(onlineDisplayID) != 0 {
if mirrorBreak == false {
CGBeginDisplayConfiguration(&displayConfigRef)
}
CGConfigureDisplayMirrorOfDisplay(displayConfigRef, onlineDisplayID, kCGNullDirectDisplay)
mirrorBreak = true
}
}
if mirrorBreak {
CGCompleteDisplayConfiguration(displayConfigRef, CGConfigureOption.permanently)
return true
}
// Build display mirror
var mainDisplayId = kCGNullDirectDisplay
for onlineDisplayID in onlineDisplayIDs where onlineDisplayID != 0 {
if CGDisplayIsBuiltin(onlineDisplayID) == 0, mainDisplayId == kCGNullDirectDisplay {
mainDisplayId = onlineDisplayID
}
}
guard mainDisplayId != kCGNullDirectDisplay else {
return false
}
CGBeginDisplayConfiguration(&displayConfigRef)
for onlineDisplayID in onlineDisplayIDs where onlineDisplayID != 0 && onlineDisplayID != mainDisplayId {
CGConfigureDisplayMirrorOfDisplay(displayConfigRef, onlineDisplayID, mainDisplayId)
}
CGCompleteDisplayConfiguration(displayConfigRef, CGConfigureOption.permanently)
return true
}
static func resolveEffectiveDisplayID(_ displayID: CGDirectDisplayID) -> CGDirectDisplayID {
var realDisplayID = displayID
if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 {
let mirroredDisplayID = CGDisplayMirrorsDisplay(displayID)
if mirroredDisplayID != 0 {
realDisplayID = mirroredDisplayID
}
}
return realDisplayID
}
static func isAppleDisplay(displayID: CGDirectDisplayID) -> Bool {
if #available(macOS 15.0, *) {
if CGDisplayVendorNumber(displayID) != 1552, CGSIsHDRSupported(displayID), CGSIsHDREnabled(displayID) {
return CGDisplayIsBuiltin(displayID) != 0
}
}
var brightness: Float = -1
let ret = DisplayServicesGetBrightness(displayID, &brightness)
if ret == 0, brightness >= 0 { // If brightness read appears to be successful using DisplayServices then it should be an Apple display
return true
}
return CGDisplayIsBuiltin(displayID) != 0 // If built-in display, it should be Apple
}
static func getByDisplayID(displayID: CGDirectDisplayID) -> NSScreen? {
NSScreen.screens.first { $0.displayID == displayID }
}
static func getDisplayRawNameByID(displayID: CGDirectDisplayID) -> String {
let defaultName = ""
if !DEBUG_MACOS10, #available(macOS 11.0, *) {
if let dictionary = (CoreDisplay_DisplayCreateInfoDictionary(displayID)?.takeRetainedValue() as NSDictionary?), let nameList = dictionary["DisplayProductName"] as? [String: String], let name = nameList["en_US"] ?? nameList.first?.value {
return name
}
}
if let screen = getByDisplayID(displayID: displayID) {
return screen.displayName ?? defaultName
}
return defaultName
}
static func getDisplayNameByID(displayID: CGDirectDisplayID) -> String {
let defaultName: String = NSLocalizedString("Unknown", comment: "Unknown display name")
if !DEBUG_MACOS10, #available(macOS 11.0, *) {
if let dictionary = (CoreDisplay_DisplayCreateInfoDictionary(displayID)?.takeRetainedValue() as NSDictionary?), let nameList = dictionary["DisplayProductName"] as? [String: String], var name = nameList[Locale.current.identifier] ?? nameList["en_US"] ?? nameList.first?.value {
if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 {
let mirroredDisplayID = CGDisplayMirrorsDisplay(displayID)
if mirroredDisplayID != 0, let dictionary = (CoreDisplay_DisplayCreateInfoDictionary(mirroredDisplayID)?.takeRetainedValue() as NSDictionary?), let nameList = dictionary["DisplayProductName"] as? [String: String], let mirroredName = nameList[Locale.current.identifier] ?? nameList["en_US"] ?? nameList.first?.value {
name.append(" | " + mirroredName)
}
}
return name
}
}
if let screen = getByDisplayID(displayID: displayID) { // MARK: This, and NSScreen+Extension.swift will not be needed when we drop MacOS 10 support.
if #available(macOS 10.15, *) {
return screen.localizedName
} else {
return screen.displayName ?? defaultName
}
}
return defaultName
}
}