mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-06-30 06:02:00 -06:00
Add DisplayLink brightness and contrast control
This commit is contained in:
parent
3cfc40598a
commit
dd2adc8769
7 changed files with 557 additions and 5 deletions
|
|
@ -38,6 +38,7 @@
|
|||
AA78BDBD2709FE7B00CA8DF7 /* UpdaterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA78BDBC2709FE7B00CA8DF7 /* UpdaterDelegate.swift */; };
|
||||
AA99521726FE25AB00612E07 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA99521626FE25AB00612E07 /* AppDelegate.swift */; };
|
||||
AA99521926FE49A300612E07 /* MenuHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA99521826FE49A300612E07 /* MenuHandler.swift */; };
|
||||
AAB41E2D2C1F6A8200E22A10 /* DisplayLinkControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB41E2C2C1F6A8200E22A10 /* DisplayLinkControl.swift */; };
|
||||
AA9AE86F26B5BF3D00B6CA65 /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA9AE86E26B5BF3D00B6CA65 /* OSD.framework */; };
|
||||
AA9AE87126B5BFB700B6CA65 /* CoreDisplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA9AE87026B5BFB700B6CA65 /* CoreDisplay.framework */; };
|
||||
AAB2F638273ED099004AB5A4 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = AAB2F637273ED099004AB5A4 /* .swiftlint.yml */; };
|
||||
|
|
@ -125,6 +126,7 @@
|
|||
AA90102027C56A0E00CC1DF7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AA99521626FE25AB00612E07 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
AA99521826FE49A300612E07 /* MenuHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuHandler.swift; sourceTree = "<group>"; };
|
||||
AAB41E2C2C1F6A8200E22A10 /* DisplayLinkControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLinkControl.swift; sourceTree = "<group>"; };
|
||||
AA99E81527622EBE00413316 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Main.strings"; sourceTree = "<group>"; };
|
||||
AA99E81627622EBE00413316 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InternetAccessPolicy.strings"; sourceTree = "<group>"; };
|
||||
AA99E81727622EBE00413316 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
|
|
@ -314,6 +316,7 @@
|
|||
6C85EFD922C941B000227EA1 /* DisplayManager.swift */,
|
||||
AA25F6D626E68C160087F3A2 /* MediaKeyTapManager.swift */,
|
||||
AA44E70627038F7F00E06865 /* KeyboardShortcutsManager.swift */,
|
||||
AAB41E2C2C1F6A8200E22A10 /* DisplayLinkControl.swift */,
|
||||
AA16139A26BE772E00DCF027 /* Arm64DDC.swift */,
|
||||
AA4398A826DD55DA00943F16 /* IntelDDC.swift */,
|
||||
FE4E0895249D584C003A50BB /* OSDUtils.swift */,
|
||||
|
|
@ -636,6 +639,7 @@
|
|||
AA44E7052703790100E06865 /* KeyboardShortcuts+Extension.swift in Sources */,
|
||||
F0A489C4279C71B200BEDFD6 /* OnboardingViewController.swift in Sources */,
|
||||
AA16139B26BE772E00DCF027 /* Arm64DDC.swift in Sources */,
|
||||
AAB41E2D2C1F6A8200E22A10 /* DisplayLinkControl.swift in Sources */,
|
||||
F0445D3820023E710025AE82 /* MainPrefsViewController.swift in Sources */,
|
||||
28D1DDF2227FBE71004CB494 /* NSScreen+Extension.swift in Sources */,
|
||||
AA99521726FE25AB00612E07 /* AppDelegate.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class OtherDisplay: Display {
|
|||
var ddc: IntelDDC?
|
||||
var arm64ddc: Bool = false
|
||||
var arm64avService: IOAVService?
|
||||
var displayLinkDisplay: DisplayLinkDisplay?
|
||||
var isDiscouraged: Bool = false
|
||||
let writeDDCQueue = DispatchQueue(label: "Local write DDC queue")
|
||||
var writeDDCNextValue: [Command: UInt16] = [:]
|
||||
|
|
@ -200,14 +201,20 @@ class OtherDisplay: Display {
|
|||
}
|
||||
|
||||
func stepContrast(isUp: Bool, isSmallIncrement: Bool) {
|
||||
guard !self.readPrefAsBool(key: .unavailableDDC, for: .contrast), !self.isSw() else {
|
||||
guard !self.readPrefAsBool(key: .unavailableDDC, for: .contrast), !self.isSw() || self.hasDisplayLinkContrastControl() else {
|
||||
return
|
||||
}
|
||||
let currentValue = self.readPrefAsFloat(for: .contrast)
|
||||
let contrastOSDValue = self.calcNewValue(currentValue: currentValue, isUp: isUp, isSmallIncrement: isSmallIncrement)
|
||||
let isAlreadySet = contrastOSDValue == self.readPrefAsFloat(for: .contrast)
|
||||
if !isAlreadySet {
|
||||
self.writeDDCValues(command: .contrast, value: self.convValueToDDC(for: .contrast, from: contrastOSDValue))
|
||||
if self.hasDisplayLinkContrastControl() {
|
||||
if !self.setDisplayLinkContrast(contrastOSDValue) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
self.writeDDCValues(command: .contrast, value: self.convValueToDDC(for: .contrast, from: contrastOSDValue))
|
||||
}
|
||||
}
|
||||
OSDUtils.showOsd(displayID: self.identifier, command: .contrast, value: contrastOSDValue, roundChiclet: !isSmallIncrement)
|
||||
if !isAlreadySet {
|
||||
|
|
@ -328,11 +335,34 @@ class OtherDisplay: Display {
|
|||
|
||||
override func setBrightness(_ to: Float = -1, slow: Bool = false) -> Bool {
|
||||
self.checkGammaInterference()
|
||||
if self.hasDisplayLinkBrightnessControl() {
|
||||
let value = to == -1 ? self.readPrefAsFloat(for: .brightness) : to
|
||||
return self.setDirectBrightness(value)
|
||||
}
|
||||
return super.setBrightness(to, slow: slow)
|
||||
}
|
||||
|
||||
override func setDirectBrightness(_ to: Float, transient: Bool = false) -> Bool {
|
||||
let value = max(min(to, 1), 0)
|
||||
if self.hasDisplayLinkBrightnessControl() {
|
||||
if DisplayLinkControl.shared.setBrightness(for: self.identifier, value: value) {
|
||||
if self.readPrefAsFloat(key: .SwBrightness) != 1 {
|
||||
_ = self.setSwBrightness(1)
|
||||
} else {
|
||||
self.savePref(1, key: .SwBrightness)
|
||||
}
|
||||
_ = DisplayManager.shared.destroyShade(displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
|
||||
if !transient {
|
||||
self.savePref(value, for: .brightness)
|
||||
self.brightnessSyncSourceValue = value
|
||||
self.smoothBrightnessTransient = value
|
||||
}
|
||||
return true
|
||||
}
|
||||
os_log("DisplayLink brightness write failed for display %{public}@. Falling back to software brightness.", type: .info, String(self.identifier))
|
||||
_ = super.setDirectBrightness(to, transient: transient)
|
||||
return true
|
||||
}
|
||||
if !self.isSw() {
|
||||
if !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue) {
|
||||
var brightnessValue: Float = 0
|
||||
|
|
@ -362,7 +392,58 @@ class OtherDisplay: Display {
|
|||
}
|
||||
|
||||
override func getBrightness() -> Float {
|
||||
self.prefExists(for: .brightness) ? self.readPrefAsFloat(for: .brightness) : 1
|
||||
if let displayLinkDisplay = self.displayLinkDisplay, !self.prefExists(for: .brightness) {
|
||||
return displayLinkDisplay.brightness
|
||||
}
|
||||
return self.prefExists(for: .brightness) ? self.readPrefAsFloat(for: .brightness) : 1
|
||||
}
|
||||
|
||||
func bindDisplayLinkDisplay(_ display: DisplayLinkDisplay) {
|
||||
self.applyDisplayLinkDisplay(display, updateSliders: false)
|
||||
}
|
||||
|
||||
func applyDisplayLinkDisplay(_ display: DisplayLinkDisplay, updateSliders: Bool = true) {
|
||||
self.displayLinkDisplay = display
|
||||
guard display.isEnabled else {
|
||||
return
|
||||
}
|
||||
self.savePref(display.brightness, for: .brightness)
|
||||
self.savePref(1, key: .SwBrightness)
|
||||
self.brightnessSyncSourceValue = display.brightness
|
||||
self.smoothBrightnessTransient = display.brightness
|
||||
if updateSliders, let slider = self.sliderHandler[.brightness] {
|
||||
slider.setValue(display.brightness, displayID: self.identifier)
|
||||
}
|
||||
if let contrast = display.contrast {
|
||||
self.savePref(contrast, for: .contrast)
|
||||
self.savePref(DDC_MAX_DETECT_LIMIT, key: .maxDDC, for: .contrast)
|
||||
if updateSliders, let slider = self.sliderHandler[.contrast] {
|
||||
slider.setValue(contrast, displayID: self.identifier)
|
||||
}
|
||||
}
|
||||
self.savePref(DDC_MAX_DETECT_LIMIT, key: .maxDDC, for: .brightness)
|
||||
_ = DisplayManager.shared.destroyShade(displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
|
||||
}
|
||||
|
||||
func hasDisplayLinkBrightnessControl() -> Bool {
|
||||
self.displayLinkDisplay?.isEnabled ?? false
|
||||
}
|
||||
|
||||
func hasDisplayLinkContrastControl() -> Bool {
|
||||
(self.displayLinkDisplay?.isEnabled ?? false) && self.displayLinkDisplay?.contrast != nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setDisplayLinkContrast(_ value: Float) -> Bool {
|
||||
guard self.hasDisplayLinkContrastControl() else {
|
||||
return false
|
||||
}
|
||||
if DisplayLinkControl.shared.setContrast(for: self.identifier, value: value) {
|
||||
self.savePref(value, for: .contrast)
|
||||
return true
|
||||
}
|
||||
os_log("DisplayLink contrast write failed for display %{public}@.", type: .info, String(self.identifier))
|
||||
return false
|
||||
}
|
||||
|
||||
func getRemapControlCodes(command: Command) -> [UInt8] {
|
||||
|
|
|
|||
398
MonitorControl/Support/DisplayLinkControl.swift
Normal file
398
MonitorControl/Support/DisplayLinkControl.swift
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
struct DisplayLinkDisplay {
|
||||
let cgID: CGDirectDisplayID
|
||||
let persistentDisplayId: String
|
||||
let name: String
|
||||
let isEnabled: Bool
|
||||
let brightness: Float
|
||||
let contrast: Float?
|
||||
}
|
||||
|
||||
final class DisplayLinkControl {
|
||||
static let shared = DisplayLinkControl()
|
||||
static let displayDidUpdateNotification = Notification.Name("MonitorControl.DisplayLinkDisplayDidUpdate")
|
||||
static let userInfoDisplayIDKey = "displayID"
|
||||
static let userInfoBrightnessKey = "brightness"
|
||||
static let userInfoContrastKey = "contrast"
|
||||
|
||||
private enum ControlKind: Hashable {
|
||||
case brightness
|
||||
case contrast
|
||||
|
||||
var requestName: String {
|
||||
switch self {
|
||||
case .brightness: return "com.displaylink.SetBrightness"
|
||||
case .contrast: return "com.displaylink.SetContrast"
|
||||
}
|
||||
}
|
||||
|
||||
var updateName: String {
|
||||
switch self {
|
||||
case .brightness: return "com.displaylink.BrightnessUpdated"
|
||||
case .contrast: return "com.displaylink.ContrastUpdated"
|
||||
}
|
||||
}
|
||||
|
||||
var payloadKey: String {
|
||||
switch self {
|
||||
case .brightness: return "brightness"
|
||||
case .contrast: return "contrast"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DisplayPayload: Decodable {
|
||||
let persistentDisplayId: String
|
||||
let isEnabled: Bool?
|
||||
let brightness: Float?
|
||||
let contrast: Float?
|
||||
let CGID: UInt32?
|
||||
let name: String?
|
||||
}
|
||||
|
||||
private struct UpdatePayload: Decodable {
|
||||
let persistentDisplayId: String?
|
||||
let statusCode: Int?
|
||||
let brightness: Float?
|
||||
let contrast: Float?
|
||||
}
|
||||
|
||||
private struct DisplayWriteKey: Hashable {
|
||||
let displayID: CGDirectDisplayID
|
||||
let kind: ControlKind
|
||||
}
|
||||
|
||||
private struct LocalWriteTarget {
|
||||
let value: Float
|
||||
let createdAt: Date
|
||||
}
|
||||
|
||||
private let notificationCenter = DistributedNotificationCenter.default()
|
||||
private let timeout: TimeInterval = 1.5
|
||||
private let writeCoalescingDelay: TimeInterval = 0.08
|
||||
private let localWriteIgnoreInterval: TimeInterval = 3
|
||||
private let stateQueue = DispatchQueue(label: "MonitorControl DisplayLink state queue")
|
||||
private let writeQueue = DispatchQueue(label: "MonitorControl DisplayLink write queue")
|
||||
private var observerTokens: [NSObjectProtocol] = []
|
||||
private var displaysByID: [CGDirectDisplayID: DisplayLinkDisplay] = [:]
|
||||
private var displayIDsByPersistentID: [String: CGDirectDisplayID] = [:]
|
||||
private var pendingWrites: [DisplayWriteKey: Float] = [:]
|
||||
private var scheduledWrites: Set<DisplayWriteKey> = []
|
||||
private var localWriteTargets: [DisplayWriteKey: LocalWriteTarget] = [:]
|
||||
|
||||
private init() {
|
||||
self.startObserving()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func refreshDisplays() -> [DisplayLinkDisplay] {
|
||||
let note = self.waitForNotification(name: "com.displaylink.DisplayListUpdated", timeout: self.timeout) {
|
||||
self.notificationCenter.postNotificationName(Notification.Name("com.displaylink.GetDisplays"), object: nil, userInfo: nil, deliverImmediately: true)
|
||||
}
|
||||
guard let raw = Self.objectString(note) else {
|
||||
os_log("DisplayLink display query timed out or returned no data.", type: .info)
|
||||
self.replaceDisplays([], notify: false)
|
||||
return []
|
||||
}
|
||||
guard let displays = Self.decodeDisplays(from: raw) else {
|
||||
os_log("DisplayLink display query returned unparseable payload: %{public}@", type: .error, raw)
|
||||
self.replaceDisplays([], notify: false)
|
||||
return []
|
||||
}
|
||||
self.replaceDisplays(displays, notify: false)
|
||||
os_log("DisplayLink display query found %{public}@ display(s).", type: .info, String(displays.count))
|
||||
return displays
|
||||
}
|
||||
|
||||
func display(for displayID: CGDirectDisplayID) -> DisplayLinkDisplay? {
|
||||
self.stateQueue.sync {
|
||||
self.displaysByID[displayID]
|
||||
}
|
||||
}
|
||||
|
||||
func setBrightness(for displayID: CGDirectDisplayID, value: Float) -> Bool {
|
||||
self.enqueueValue(kind: .brightness, for: displayID, value: value)
|
||||
}
|
||||
|
||||
func setContrast(for displayID: CGDirectDisplayID, value: Float) -> Bool {
|
||||
self.enqueueValue(kind: .contrast, for: displayID, value: value)
|
||||
}
|
||||
|
||||
private func startObserving() {
|
||||
self.observerTokens.append(self.notificationCenter.addObserver(forName: Notification.Name("com.displaylink.DisplayListUpdated"), object: nil, queue: .main) { [weak self] note in
|
||||
self?.handleDisplayListNotification(note)
|
||||
})
|
||||
self.observerTokens.append(self.notificationCenter.addObserver(forName: Notification.Name("com.displaylink.BrightnessUpdated"), object: nil, queue: .main) { [weak self] note in
|
||||
self?.handleUpdateNotification(note, kind: .brightness)
|
||||
})
|
||||
self.observerTokens.append(self.notificationCenter.addObserver(forName: Notification.Name("com.displaylink.ContrastUpdated"), object: nil, queue: .main) { [weak self] note in
|
||||
self?.handleUpdateNotification(note, kind: .contrast)
|
||||
})
|
||||
}
|
||||
|
||||
private func enqueueValue(kind: ControlKind, for displayID: CGDirectDisplayID, value: Float) -> Bool {
|
||||
guard let display = self.display(for: displayID), display.isEnabled else {
|
||||
return false
|
||||
}
|
||||
let normalizedValue = max(min(value, 1), 0)
|
||||
let key = DisplayWriteKey(displayID: displayID, kind: kind)
|
||||
self.setLocalWriteTarget(key, value: normalizedValue)
|
||||
self.writeQueue.async {
|
||||
self.pendingWrites[key] = normalizedValue
|
||||
guard !self.scheduledWrites.contains(key) else {
|
||||
return
|
||||
}
|
||||
self.scheduledWrites.insert(key)
|
||||
self.writeQueue.asyncAfter(deadline: .now() + self.writeCoalescingDelay) {
|
||||
self.flushQueuedValue(for: key)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func flushQueuedValue(for key: DisplayWriteKey) {
|
||||
guard let value = self.pendingWrites.removeValue(forKey: key) else {
|
||||
self.scheduledWrites.remove(key)
|
||||
return
|
||||
}
|
||||
if !self.writeValueSynchronously(kind: key.kind, for: key.displayID, value: value) {
|
||||
self.clearLocalWriteTarget(key, force: true)
|
||||
}
|
||||
if self.pendingWrites[key] != nil {
|
||||
self.writeQueue.asyncAfter(deadline: .now() + self.writeCoalescingDelay) {
|
||||
self.flushQueuedValue(for: key)
|
||||
}
|
||||
} else {
|
||||
self.scheduledWrites.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
private func writeValueSynchronously(kind: ControlKind, for displayID: CGDirectDisplayID, value: Float) -> Bool {
|
||||
guard let display = self.display(for: displayID), display.isEnabled else {
|
||||
return false
|
||||
}
|
||||
guard let payload = Self.jsonString([
|
||||
"persistentDisplayId": display.persistentDisplayId,
|
||||
kind.payloadKey: Double(value),
|
||||
]) else {
|
||||
return false
|
||||
}
|
||||
let note = self.waitForNotification(name: kind.updateName, timeout: self.timeout) {
|
||||
self.notificationCenter.postNotificationName(Notification.Name(kind.requestName), object: payload, userInfo: nil, deliverImmediately: true)
|
||||
} filter: { note in
|
||||
guard let raw = Self.objectString(note),
|
||||
let update = try? JSONDecoder().decode(UpdatePayload.self, from: Data(raw.utf8)) else {
|
||||
return false
|
||||
}
|
||||
return update.persistentDisplayId == display.persistentDisplayId
|
||||
}
|
||||
guard let raw = Self.objectString(note),
|
||||
let update = try? JSONDecoder().decode(UpdatePayload.self, from: Data(raw.utf8)),
|
||||
update.statusCode == 0 else {
|
||||
os_log("DisplayLink %{public}@ write failed for display %{public}@.", type: .info, kind.payloadKey, display.persistentDisplayId)
|
||||
return false
|
||||
}
|
||||
guard let acknowledgedValue = self.updatedValue(kind: kind, fallback: value, update: update) else {
|
||||
return false
|
||||
}
|
||||
self.updateCache(displayID: displayID, kind: kind, value: acknowledgedValue, notify: !self.hasActiveLocalWriteTarget(key: DisplayWriteKey(displayID: displayID, kind: kind)))
|
||||
self.clearLocalWriteTarget(DisplayWriteKey(displayID: displayID, kind: kind), acknowledgedValue: acknowledgedValue)
|
||||
return true
|
||||
}
|
||||
|
||||
private func handleDisplayListNotification(_ note: Notification) {
|
||||
guard let raw = Self.objectString(note), let displays = Self.decodeDisplays(from: raw) else {
|
||||
return
|
||||
}
|
||||
self.replaceDisplays(displays, notify: true)
|
||||
}
|
||||
|
||||
private func handleUpdateNotification(_ note: Notification, kind: ControlKind) {
|
||||
guard let raw = Self.objectString(note),
|
||||
let update = try? JSONDecoder().decode(UpdatePayload.self, from: Data(raw.utf8)),
|
||||
(update.statusCode ?? 0) == 0,
|
||||
let persistentDisplayId = update.persistentDisplayId else {
|
||||
return
|
||||
}
|
||||
let value = self.updatedValue(kind: kind, fallback: nil, update: update)
|
||||
guard let value else {
|
||||
return
|
||||
}
|
||||
guard let displayID = self.displayID(forPersistentDisplayId: persistentDisplayId) else {
|
||||
return
|
||||
}
|
||||
let key = DisplayWriteKey(displayID: displayID, kind: kind)
|
||||
self.updateCache(displayID: displayID, kind: kind, value: value, notify: !self.hasActiveLocalWriteTarget(key: key))
|
||||
self.clearLocalWriteTarget(key, acknowledgedValue: value)
|
||||
}
|
||||
|
||||
private func displayID(forPersistentDisplayId persistentDisplayId: String) -> CGDirectDisplayID? {
|
||||
self.stateQueue.sync {
|
||||
self.displayIDsByPersistentID[persistentDisplayId]
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func updateCache(displayID: CGDirectDisplayID, kind: ControlKind, value: Float, notify: Bool) -> DisplayLinkDisplay? {
|
||||
var updatedDisplay: DisplayLinkDisplay?
|
||||
self.stateQueue.sync {
|
||||
guard let current = self.displaysByID[displayID] else {
|
||||
return
|
||||
}
|
||||
let brightness = kind == .brightness ? value : current.brightness
|
||||
let contrast = kind == .contrast ? value : current.contrast
|
||||
let updated = DisplayLinkDisplay(
|
||||
cgID: current.cgID,
|
||||
persistentDisplayId: current.persistentDisplayId,
|
||||
name: current.name,
|
||||
isEnabled: current.isEnabled,
|
||||
brightness: brightness,
|
||||
contrast: contrast
|
||||
)
|
||||
self.displaysByID[displayID] = updated
|
||||
self.displayIDsByPersistentID[updated.persistentDisplayId] = displayID
|
||||
updatedDisplay = updated
|
||||
}
|
||||
if notify, let updatedDisplay {
|
||||
self.postDisplayUpdate(updatedDisplay)
|
||||
}
|
||||
return updatedDisplay
|
||||
}
|
||||
|
||||
private func replaceDisplays(_ displays: [DisplayLinkDisplay], notify: Bool) {
|
||||
self.stateQueue.sync {
|
||||
var displaysByID: [CGDirectDisplayID: DisplayLinkDisplay] = [:]
|
||||
var displayIDsByPersistentID: [String: CGDirectDisplayID] = [:]
|
||||
for display in displays {
|
||||
displaysByID[display.cgID] = display
|
||||
displayIDsByPersistentID[display.persistentDisplayId] = display.cgID
|
||||
}
|
||||
self.displaysByID = displaysByID
|
||||
self.displayIDsByPersistentID = displayIDsByPersistentID
|
||||
}
|
||||
if notify {
|
||||
for display in displays {
|
||||
if self.shouldNotifyUpdate(for: display.cgID) {
|
||||
self.postDisplayUpdate(display)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func postDisplayUpdate(_ display: DisplayLinkDisplay) {
|
||||
var userInfo: [String: Any] = [
|
||||
Self.userInfoDisplayIDKey: display.cgID,
|
||||
Self.userInfoBrightnessKey: display.brightness,
|
||||
]
|
||||
if let contrast = display.contrast {
|
||||
userInfo[Self.userInfoContrastKey] = contrast
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: Self.displayDidUpdateNotification, object: self, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private func setLocalWriteTarget(_ key: DisplayWriteKey, value: Float) {
|
||||
self.stateQueue.sync {
|
||||
self.localWriteTargets[key] = LocalWriteTarget(value: value, createdAt: Date())
|
||||
}
|
||||
}
|
||||
|
||||
private func hasActiveLocalWriteTarget(key: DisplayWriteKey) -> Bool {
|
||||
self.stateQueue.sync {
|
||||
guard let target = self.localWriteTargets[key] else {
|
||||
return false
|
||||
}
|
||||
if Date().timeIntervalSince(target.createdAt) > self.localWriteIgnoreInterval {
|
||||
self.localWriteTargets.removeValue(forKey: key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func clearLocalWriteTarget(_ key: DisplayWriteKey, acknowledgedValue: Float? = nil, force: Bool = false) {
|
||||
self.stateQueue.sync {
|
||||
guard force || acknowledgedValue != nil else {
|
||||
return
|
||||
}
|
||||
if force {
|
||||
self.localWriteTargets.removeValue(forKey: key)
|
||||
return
|
||||
}
|
||||
if let target = self.localWriteTargets[key], let acknowledgedValue, abs(target.value - acknowledgedValue) < 0.002 {
|
||||
self.localWriteTargets.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldNotifyUpdate(for displayID: CGDirectDisplayID) -> Bool {
|
||||
!self.hasActiveLocalWriteTarget(key: DisplayWriteKey(displayID: displayID, kind: .brightness)) && !self.hasActiveLocalWriteTarget(key: DisplayWriteKey(displayID: displayID, kind: .contrast))
|
||||
}
|
||||
|
||||
private func updatedValue(kind: ControlKind, fallback: Float?, update: UpdatePayload) -> Float? {
|
||||
switch kind {
|
||||
case .brightness:
|
||||
return update.brightness ?? fallback
|
||||
case .contrast:
|
||||
return update.contrast ?? fallback
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForNotification(name: String, timeout: TimeInterval, trigger: () -> Void, filter: ((Notification) -> Bool)? = nil) -> Notification? {
|
||||
var received: Notification?
|
||||
let token = self.notificationCenter.addObserver(forName: Notification.Name(name), object: nil, queue: nil) { note in
|
||||
if filter?(note) ?? true {
|
||||
received = note
|
||||
}
|
||||
}
|
||||
trigger()
|
||||
let until = Date().addingTimeInterval(timeout)
|
||||
while received == nil, Date() < until {
|
||||
RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
|
||||
}
|
||||
self.notificationCenter.removeObserver(token)
|
||||
return received
|
||||
}
|
||||
|
||||
private static func objectString(_ note: Notification?) -> String? {
|
||||
if let string = note?.object as? String {
|
||||
return string
|
||||
}
|
||||
if let string = note?.object as? NSString {
|
||||
return string as String
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func decodeDisplays(from raw: String) -> [DisplayLinkDisplay]? {
|
||||
guard let payloads = try? JSONDecoder().decode([DisplayPayload].self, from: Data(raw.utf8)) else {
|
||||
return nil
|
||||
}
|
||||
return payloads.compactMap { payload -> DisplayLinkDisplay? in
|
||||
guard let cgID = payload.CGID, let brightness = payload.brightness else {
|
||||
return nil
|
||||
}
|
||||
return DisplayLinkDisplay(
|
||||
cgID: CGDirectDisplayID(cgID),
|
||||
persistentDisplayId: payload.persistentDisplayId,
|
||||
name: payload.name ?? payload.persistentDisplayId,
|
||||
isEnabled: payload.isEnabled ?? true,
|
||||
brightness: brightness,
|
||||
contrast: payload.contrast
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func jsonString(_ object: [String: Any]) -> String? {
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
|
||||
return nil
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,13 @@ class DisplayManager {
|
|||
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
|
||||
private var displayLinkUpdateObserver: NSObjectProtocol?
|
||||
|
||||
private init() {
|
||||
self.displayLinkUpdateObserver = NotificationCenter.default.addObserver(forName: DisplayLinkControl.displayDidUpdateNotification, object: nil, queue: .main) { [weak self] note in
|
||||
self?.displayLinkDisplayDidUpdate(note)
|
||||
}
|
||||
}
|
||||
|
||||
func createGammaActivityEnforcer() {
|
||||
self.gammaActivityEnforcer.title = "Monitor Control Gamma Activity Enforcer"
|
||||
|
|
@ -160,8 +167,35 @@ class DisplayManager {
|
|||
return false
|
||||
}
|
||||
|
||||
private func displayLinkDisplayDidUpdate(_ note: Notification) {
|
||||
guard app != nil, app.sleepID == 0, app.reconfigureID == 0,
|
||||
let displayID = self.displayLinkDisplayID(from: note.userInfo),
|
||||
let displayLinkDisplay = DisplayLinkControl.shared.display(for: displayID),
|
||||
let otherDisplay = self.getOtherDisplays().first(where: { $0.identifier == displayID }) else {
|
||||
return
|
||||
}
|
||||
otherDisplay.applyDisplayLinkDisplay(displayLinkDisplay)
|
||||
}
|
||||
|
||||
private func displayLinkDisplayID(from userInfo: [AnyHashable: Any]?) -> CGDirectDisplayID? {
|
||||
guard let rawDisplayID = userInfo?[DisplayLinkControl.userInfoDisplayIDKey] else {
|
||||
return nil
|
||||
}
|
||||
if let displayID = rawDisplayID as? CGDirectDisplayID {
|
||||
return displayID
|
||||
}
|
||||
if let displayID = rawDisplayID as? UInt32 {
|
||||
return CGDirectDisplayID(displayID)
|
||||
}
|
||||
if let displayID = rawDisplayID as? NSNumber {
|
||||
return CGDirectDisplayID(displayID.uint32Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureDisplays() {
|
||||
self.clearDisplays()
|
||||
DisplayLinkControl.shared.refreshDisplays()
|
||||
var onlineDisplayIDs = [CGDirectDisplayID](repeating: 0, count: 16)
|
||||
var displayCount: UInt32 = 0
|
||||
guard CGGetOnlineDisplayList(16, &onlineDisplayIDs, &displayCount) == .success else {
|
||||
|
|
@ -183,6 +217,10 @@ class DisplayManager {
|
|||
self.addDisplay(display: appleDisplay)
|
||||
} else {
|
||||
let otherDisplay = OtherDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, serialNumber: serialNumber, isVirtual: isVirtual, isDummy: isDummy)
|
||||
if let displayLinkDisplay = DisplayLinkControl.shared.display(for: id) {
|
||||
otherDisplay.bindDisplayLinkDisplay(displayLinkDisplay)
|
||||
os_log("DisplayLink display matched - %{public}@", type: .info, "ID: \(otherDisplay.identifier), Persistent ID: \(displayLinkDisplay.persistentDisplayId)")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
@ -191,6 +229,21 @@ class DisplayManager {
|
|||
|
||||
func setupOtherDisplays(firstrun: Bool = false) {
|
||||
for otherDisplay in self.getOtherDisplays() {
|
||||
if otherDisplay.hasDisplayLinkBrightnessControl() {
|
||||
if !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: .brightness) {
|
||||
let brightness = otherDisplay.displayLinkDisplay?.brightness ?? otherDisplay.readPrefAsFloat(for: .brightness)
|
||||
otherDisplay.savePref(brightness, for: .brightness)
|
||||
otherDisplay.savePref(1, key: .SwBrightness)
|
||||
otherDisplay.brightnessSyncSourceValue = brightness
|
||||
otherDisplay.smoothBrightnessTransient = brightness
|
||||
_ = DisplayManager.shared.destroyShade(displayID: DisplayManager.resolveEffectiveDisplayID(otherDisplay.identifier))
|
||||
}
|
||||
if otherDisplay.hasDisplayLinkContrastControl(), !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: .contrast), let contrast = otherDisplay.displayLinkDisplay?.contrast {
|
||||
otherDisplay.savePref(contrast, for: .contrast)
|
||||
otherDisplay.savePref(DDC_MAX_DETECT_LIMIT, key: .maxDDC, for: .contrast)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for command in [Command.audioSpeakerVolume, Command.contrast] where !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: command) && !otherDisplay.isSw() {
|
||||
otherDisplay.setupCurrentAndMaxValues(command: command, firstrun: firstrun)
|
||||
}
|
||||
|
|
@ -379,6 +432,14 @@ class DisplayManager {
|
|||
|
||||
func restoreSwBrightnessForAllDisplays(async: Bool = false) {
|
||||
for otherDisplay in self.getOtherDisplays() {
|
||||
if otherDisplay.hasDisplayLinkBrightnessControl() {
|
||||
_ = otherDisplay.setSwBrightness(1, smooth: async)
|
||||
_ = DisplayManager.shared.destroyShade(displayID: DisplayManager.resolveEffectiveDisplayID(otherDisplay.identifier))
|
||||
if let slider = otherDisplay.sliderHandler[.brightness] {
|
||||
slider.setValue(otherDisplay.readPrefAsFloat(for: .brightness), displayID: otherDisplay.identifier)
|
||||
}
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class MenuHandler: NSMenu, NSMenuDelegate {
|
|||
addedSliderHandlers.append(self.setupMenuSliderHandler(command: .audioSpeakerVolume, display: display, title: title))
|
||||
}
|
||||
display.sliderHandler[.contrast] = nil
|
||||
if let otherDisplay = display as? OtherDisplay, !otherDisplay.isSw(), !display.readPrefAsBool(key: .unavailableDDC, for: .contrast), prefs.bool(forKey: PrefKey.showContrast.rawValue) {
|
||||
if let otherDisplay = display as? OtherDisplay, (!otherDisplay.isSw() || otherDisplay.hasDisplayLinkContrastControl()), !display.readPrefAsBool(key: .unavailableDDC, for: .contrast), prefs.bool(forKey: PrefKey.showContrast.rawValue) {
|
||||
let title = NSLocalizedString("Contrast", comment: "Shown in menu")
|
||||
addedSliderHandlers.append(self.setupMenuSliderHandler(command: .contrast, display: display, title: title))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,6 +292,9 @@ class SliderHandler {
|
|||
if self.command == Command.brightness {
|
||||
_ = otherDisplay.setBrightness(value)
|
||||
return
|
||||
} else if self.command == Command.contrast, otherDisplay.hasDisplayLinkContrastControl() {
|
||||
_ = otherDisplay.setDisplayLinkContrast(value)
|
||||
return
|
||||
} else if !otherDisplay.isSw() {
|
||||
if self.command == Command.audioSpeakerVolume {
|
||||
if !otherDisplay.readPrefAsBool(key: .enableMuteUnmute) || value != 0 {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,12 @@ class DisplaysPrefsViewController: NSViewController, SettingsPane, NSTableViewDa
|
|||
var displayImage = "display.trianglebadge.exclamationmark"
|
||||
var controlMethod = NSLocalizedString("No Control", comment: "Shown in the Display Settings") + " ⚠️"
|
||||
var controlStatus = NSLocalizedString("This display has an unspecified control status.", comment: "Shown in the Display Settings")
|
||||
if display.isVirtual, !display.isDummy {
|
||||
if let otherDisplay = display as? OtherDisplay, otherDisplay.hasDisplayLinkBrightnessControl(), !display.isDummy {
|
||||
displayType = NSLocalizedString("Virtual Display", comment: "Shown in the Display Settings")
|
||||
displayImage = "tv.and.mediabox"
|
||||
controlMethod = NSLocalizedString("Hardware (DisplayLink)", comment: "Shown in the Display Settings")
|
||||
controlStatus = NSLocalizedString("This display supports native DisplayLink brightness and contrast control.", comment: "Shown in the Display Settings")
|
||||
} else if display.isVirtual, !display.isDummy {
|
||||
displayType = NSLocalizedString("Virtual Display", comment: "Shown in the Display Settings")
|
||||
displayImage = "tv.and.mediabox"
|
||||
controlMethod = NSLocalizedString("Software (shade)", comment: "Shown in the Display Settings") + " ⚠️"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue