mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-05-15 14:15:55 -06:00
Various fixes and improvements for v4.0.1 (#769)
- Fixed: display properties reset turns off hardware DDC - Fixed: brief black screen upon changing space when using shade dimming - Asynchronous thread-safe debouncing DDC write for smoother sliders - Improved support for [BetterDummy](https://github.com/waydabber/BetterDummy) - Better support for common physical dummies identifying as 28E850 - Inert dummy menu sliders are now hidden - Improved support for nongamma->nongamma mirroring scenarios - Compiled to run 10.14 (needed some minor changes) - compatibility is unofficial! - Updated README - Bumped version number to 4.0.1 (service release)
This commit is contained in:
parent
9f93840995
commit
cfc77d53d6
11 changed files with 142 additions and 89 deletions
|
|
@ -4,7 +4,7 @@ disabled_rules:
|
|||
- identifier_name
|
||||
- trailing_comma
|
||||
type_body_length: 500
|
||||
file_length: 500
|
||||
file_length: 750
|
||||
cyclomatic_complexity:
|
||||
ignores_case_statements: true
|
||||
opening_brace:
|
||||
|
|
|
|||
|
|
@ -751,8 +751,8 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 4.0.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -786,8 +786,8 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 4.0.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -821,7 +821,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControlHelper;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -850,7 +850,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControlHelper;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>6851</string>
|
||||
<string>6941</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ class Display: Equatable {
|
|||
return
|
||||
}
|
||||
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
|
||||
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: self.identifier)
|
||||
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
|
||||
} else {
|
||||
let gammaTableRed = self.defaultGammaTableRed.map { $0 * transientValue }
|
||||
let gammaTableGreen = self.defaultGammaTableGreen.map { $0 * transientValue }
|
||||
|
|
@ -243,7 +243,7 @@ class Display: Equatable {
|
|||
} else {
|
||||
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
|
||||
self.swBrightnessSemaphore.signal()
|
||||
return DisplayManager.shared.setShadeAlpha(value: 1 - newValue, displayID: self.identifier)
|
||||
return DisplayManager.shared.setShadeAlpha(value: 1 - newValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
|
||||
} else {
|
||||
let gammaTableRed = self.defaultGammaTableRed.map { $0 * newValue }
|
||||
let gammaTableGreen = self.defaultGammaTableGreen.map { $0 * newValue }
|
||||
|
|
@ -267,7 +267,7 @@ class Display: Equatable {
|
|||
}
|
||||
self.swBrightnessSemaphore.wait()
|
||||
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
|
||||
let rawBrightnessValue = 1 - (DisplayManager.shared.getShadeAlpha(displayID: self.identifier) ?? 1)
|
||||
let rawBrightnessValue = 1 - (DisplayManager.shared.getShadeAlpha(displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier)) ?? 1)
|
||||
self.swBrightnessSemaphore.signal()
|
||||
return self.swBrightnessTransform(value: rawBrightnessValue, reverse: true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ class OtherDisplay: Display {
|
|||
var arm64ddc: Bool = false
|
||||
var arm64avService: IOAVService?
|
||||
var isDiscouraged: Bool = false
|
||||
let writeDDCQueue = DispatchQueue(label: "Local write DDC queue")
|
||||
var writeDDCNextValue: [Command: UInt16] = [:]
|
||||
var writeDDCLastSavedValue: [Command: UInt16] = [:]
|
||||
var pollingCount: Int {
|
||||
get {
|
||||
switch self.readPrefAsInt(key: .pollingMode) {
|
||||
|
|
@ -76,11 +79,11 @@ class OtherDisplay: Display {
|
|||
if !self.smoothBrightnessRunning, !self.isSw(), !self.readPrefAsBool(key: .unavailableDDC, for: command), self.readPrefAsBool(key: .isTouched, for: command), prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue, !app.safeMode {
|
||||
let restoreValue = self.getDDCValueFromPrefs(command)
|
||||
os_log("Restoring %{public}@ DDC value %{public}@ for %{public}@", type: .info, String(reflecting: command), String(restoreValue), self.name)
|
||||
_ = self.writeDDCValues(command: command, value: restoreValue)
|
||||
self.writeDDCValues(command: command, value: restoreValue)
|
||||
if command == .audioSpeakerVolume, self.readPrefAsBool(key: .enableMuteUnmute) {
|
||||
let currentMuteValue = self.readPrefAsInt(for: .audioMuteScreenBlank) == 0 ? 2 : self.readPrefAsInt(for: .audioMuteScreenBlank)
|
||||
os_log("- Writing last saved DDC value for Mute: %{public}@", type: .info, String(currentMuteValue))
|
||||
_ = self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(currentMuteValue))
|
||||
self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(currentMuteValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,13 +181,11 @@ class OtherDisplay: Display {
|
|||
let isAlreadySet = volumeOSDValue == self.readPrefAsFloat(for: .audioSpeakerVolume)
|
||||
if !isAlreadySet {
|
||||
if let muteValue = muteValue, self.readPrefAsBool(key: .enableMuteUnmute) {
|
||||
guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else {
|
||||
return
|
||||
}
|
||||
self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue))
|
||||
self.savePref(muteValue, for: .audioMuteScreenBlank)
|
||||
}
|
||||
if !self.readPrefAsBool(key: .enableMuteUnmute) || volumeOSDValue != 0 {
|
||||
_ = self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
|
||||
self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
|
||||
}
|
||||
}
|
||||
if !self.readPrefAsBool(key: .hideOsd) {
|
||||
|
|
@ -206,7 +207,7 @@ class OtherDisplay: Display {
|
|||
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))
|
||||
self.writeDDCValues(command: .contrast, value: self.convValueToDDC(for: .contrast, from: contrastOSDValue))
|
||||
}
|
||||
OSDUtils.showOsd(displayID: self.identifier, command: .contrast, value: contrastOSDValue, roundChiclet: !isSmallIncrement)
|
||||
if !isAlreadySet {
|
||||
|
|
@ -237,13 +238,11 @@ class OtherDisplay: Display {
|
|||
}
|
||||
}
|
||||
if self.readPrefAsBool(key: .enableMuteUnmute) {
|
||||
guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else {
|
||||
return
|
||||
}
|
||||
self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue))
|
||||
}
|
||||
self.savePref(muteValue, for: .audioMuteScreenBlank)
|
||||
if !self.readPrefAsBool(key: .enableMuteUnmute) || volumeOSDValue > 0 {
|
||||
_ = self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
|
||||
self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
|
||||
}
|
||||
if !fromVolumeSlider {
|
||||
if !self.readPrefAsBool(key: .hideOsd) {
|
||||
|
|
@ -345,12 +344,12 @@ class OtherDisplay: Display {
|
|||
brightnessValue = 0
|
||||
brightnessSwValue = (value / self.combinedBrightnessSwitchingValue())
|
||||
}
|
||||
_ = self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: brightnessValue))
|
||||
self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: brightnessValue))
|
||||
if self.readPrefAsFloat(key: .SwBrightness) != brightnessSwValue {
|
||||
_ = self.setSwBrightness(brightnessSwValue)
|
||||
}
|
||||
} else {
|
||||
_ = self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: value))
|
||||
self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: value))
|
||||
}
|
||||
if !transient {
|
||||
self.savePref(value, for: .brightness)
|
||||
|
|
@ -378,28 +377,45 @@ class OtherDisplay: Display {
|
|||
return intCodes
|
||||
}
|
||||
|
||||
public func writeDDCValues(command: Command, value: UInt16, errorRecoveryWaitTime _: UInt32? = nil) -> Bool? {
|
||||
public func writeDDCValues(command: Command, value: UInt16) {
|
||||
guard app.sleepID == 0, app.reconfigureID == 0, !self.readPrefAsBool(key: .forceSw), !self.readPrefAsBool(key: .unavailableDDC, for: command) else {
|
||||
return false
|
||||
return
|
||||
}
|
||||
self.writeDDCQueue.async(flags: .barrier) {
|
||||
self.writeDDCNextValue[command] = value
|
||||
}
|
||||
DisplayManager.shared.globalDDCQueue.async(flags: .barrier) {
|
||||
self.asyncPerformWriteDDCValues(command: command)
|
||||
}
|
||||
}
|
||||
|
||||
func asyncPerformWriteDDCValues(command: Command) {
|
||||
var value = UInt16.max
|
||||
var lastValue = UInt16.max
|
||||
self.writeDDCQueue.sync {
|
||||
value = self.writeDDCNextValue[command] ?? UInt16.max
|
||||
lastValue = self.writeDDCLastSavedValue[command] ?? UInt16.max
|
||||
}
|
||||
guard value != UInt16.max, value != lastValue else {
|
||||
return
|
||||
}
|
||||
self.writeDDCQueue.async(flags: .barrier) {
|
||||
self.writeDDCLastSavedValue[command] = value
|
||||
self.savePref(true, key: PrefKey.isTouched, for: command)
|
||||
}
|
||||
var success: Bool = false
|
||||
var controlCodes = self.getRemapControlCodes(command: command)
|
||||
if controlCodes.count == 0 {
|
||||
controlCodes.append(command.rawValue)
|
||||
}
|
||||
for controlCode in controlCodes {
|
||||
DisplayManager.shared.ddcQueue.sync {
|
||||
if Arm64DDC.isArm64 {
|
||||
if self.arm64ddc {
|
||||
success = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value)
|
||||
}
|
||||
} else {
|
||||
success = self.ddc?.write(command: command.rawValue, value: value, errorRecoveryWaitTime: 2000) ?? false
|
||||
if Arm64DDC.isArm64 {
|
||||
if self.arm64ddc {
|
||||
_ = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value)
|
||||
}
|
||||
self.savePref(true, key: PrefKey.isTouched, for: command) // We deliberatly consider the value tuched no matter if the call succeeded
|
||||
} else {
|
||||
_ = self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false
|
||||
}
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
func readDDCValues(for command: Command, tries: UInt, minReplyDelay delay: UInt64?) -> (current: UInt16, max: UInt16)? {
|
||||
|
|
@ -413,7 +429,7 @@ class OtherDisplay: Display {
|
|||
guard self.arm64ddc else {
|
||||
return nil
|
||||
}
|
||||
DisplayManager.shared.ddcQueue.sync {
|
||||
DisplayManager.shared.globalDDCQueue.sync {
|
||||
if let unwrappedDelay = delay {
|
||||
values = Arm64DDC.read(service: self.arm64avService, command: controlCode, tries: UInt8(min(tries, 255)), minReplyDelay: UInt32(unwrappedDelay / 1000))
|
||||
} else {
|
||||
|
|
@ -421,7 +437,7 @@ class OtherDisplay: Display {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
DisplayManager.shared.ddcQueue.sync {
|
||||
DisplayManager.shared.globalDDCQueue.sync {
|
||||
values = self.ddc?.read(command: controlCode, tries: tries, minReplyDelay: delay)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class DisplayManager {
|
|||
|
||||
var displays: [Display] = []
|
||||
var audioControlTargetDisplays: [OtherDisplay] = []
|
||||
let ddcQueue = DispatchQueue(label: "DDC queue")
|
||||
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
|
||||
|
|
@ -43,8 +43,22 @@ class DisplayManager {
|
|||
internal var shades: [CGDirectDisplayID: NSWindow] = [:]
|
||||
internal var shadeGrave: [NSWindow] = []
|
||||
|
||||
func isDisqualifiedFromShade(_ displayID: CGDirectDisplayID) -> Bool { // We ban mirror members from shade control as it might lead to double control
|
||||
return (CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0) ? true : false
|
||||
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
|
||||
}
|
||||
|
||||
internal func createShadeOnDisplay(displayID: CGDirectDisplayID) -> NSWindow? {
|
||||
|
|
@ -52,13 +66,16 @@ class DisplayManager {
|
|||
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 = .black
|
||||
shade.backgroundColor = .clear
|
||||
shade.ignoresMouseEvents = true
|
||||
shade.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
||||
shade.alphaValue = 0
|
||||
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
|
||||
}
|
||||
|
|
@ -125,7 +142,7 @@ class DisplayManager {
|
|||
return 1
|
||||
}
|
||||
if let shade = getShade(displayID: displayID) {
|
||||
return Float(shade.alphaValue)
|
||||
return Float(shade.contentView?.alphaValue ?? 1)
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
|
|
@ -136,7 +153,7 @@ class DisplayManager {
|
|||
return false
|
||||
}
|
||||
if let shade = getShade(displayID: displayID) {
|
||||
shade.alphaValue = CGFloat(value)
|
||||
shade.contentView?.alphaValue = CGFloat(value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -151,27 +168,12 @@ class DisplayManager {
|
|||
return
|
||||
}
|
||||
for onlineDisplayID in onlineDisplayIDs where onlineDisplayID != 0 {
|
||||
let rawName = DisplayManager.getDisplayRawNameByID(displayID: onlineDisplayID)
|
||||
let name = DisplayManager.getDisplayNameByID(displayID: onlineDisplayID)
|
||||
let id = onlineDisplayID
|
||||
let vendorNumber = CGDisplayVendorNumber(onlineDisplayID)
|
||||
let modelNumber = CGDisplayModelNumber(onlineDisplayID)
|
||||
var isDummy: Bool = false
|
||||
var isVirtual: Bool = false
|
||||
if rawName == "28E850" || rawName.lowercased().contains("dummy") {
|
||||
os_log("NOTE: Display is a dummy!", type: .info)
|
||||
isDummy = true
|
||||
}
|
||||
if !DEBUG_MACOS10, #available(macOS 11.0, *) {
|
||||
if let dictionary = ((CoreDisplay_DisplayCreateInfoDictionary(onlineDisplayID))?.takeRetainedValue() as NSDictionary?) {
|
||||
let isVirtualDevice = dictionary["kCGDisplayIsVirtualDevice"] as? Bool
|
||||
let displayIsAirplay = dictionary["kCGDisplayIsAirPlay"] as? Bool
|
||||
if isVirtualDevice ?? displayIsAirplay ?? false {
|
||||
os_log("NOTE: Display is virtual!", type: .info)
|
||||
isVirtual = true
|
||||
}
|
||||
}
|
||||
}
|
||||
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, 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))")
|
||||
|
|
@ -391,6 +393,30 @@ class DisplayManager {
|
|||
return affectedDisplays
|
||||
}
|
||||
|
||||
static func isDummy(displayID: CGDirectDisplayID) -> Bool {
|
||||
let rawName = DisplayManager.getDisplayRawNameByID(displayID: displayID)
|
||||
var isDummy: Bool = false
|
||||
if rawName == "28E850" || rawName.lowercased().contains("dummy") {
|
||||
os_log("NOTE: Display is a dummy!", type: .info)
|
||||
isDummy = true
|
||||
}
|
||||
return isDummy
|
||||
}
|
||||
|
||||
static func isVirtual(displayID: CGDirectDisplayID) -> Bool {
|
||||
var isVirtual: Bool = 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
|
||||
|
|
@ -478,14 +504,18 @@ class DisplayManager {
|
|||
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)
|
||||
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.
|
||||
return screen.localizedName
|
||||
if #available(macOS 10.15, *) {
|
||||
return screen.localizedName
|
||||
} else {
|
||||
return screen.displayName ?? defaultName
|
||||
}
|
||||
}
|
||||
return defaultName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ class MenuHandler: NSMenu, NSMenuDelegate {
|
|||
displays.append(contentsOf: DisplayManager.shared.getOtherDisplays())
|
||||
let relevant = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.relevant.rawValue
|
||||
let combine = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.combine.rawValue
|
||||
let numOfDisplays = displays.count
|
||||
let numOfDisplays = displays.filter { !$0.isDummy }.count
|
||||
if numOfDisplays != 0 {
|
||||
let asSubMenu: Bool = (displays.count > 3 && !relevant && !combine && app.macOS10()) ? true : false
|
||||
var iterator = 0
|
||||
for display in displays where !relevant || display == currentDisplay {
|
||||
for display in displays where (!relevant || DisplayManager.resolveEffectiveDisplayID(display.identifier) == DisplayManager.resolveEffectiveDisplayID(currentDisplay!.identifier)) && !display.isDummy {
|
||||
iterator += 1
|
||||
if !relevant, !combine, iterator != 1, app.macOS10() {
|
||||
self.insertItem(NSMenuItem.separator(), at: 0)
|
||||
|
|
|
|||
|
|
@ -295,10 +295,10 @@ class SliderHandler {
|
|||
} else if !otherDisplay.isSw() {
|
||||
if self.command == Command.audioSpeakerVolume {
|
||||
if !otherDisplay.readPrefAsBool(key: .enableMuteUnmute) || value != 0 {
|
||||
_ = otherDisplay.writeDDCValues(command: self.command, value: otherDisplay.convValueToDDC(for: self.command, from: value))
|
||||
otherDisplay.writeDDCValues(command: self.command, value: otherDisplay.convValueToDDC(for: self.command, from: value))
|
||||
}
|
||||
} else {
|
||||
_ = otherDisplay.writeDDCValues(command: self.command, value: otherDisplay.convValueToDDC(for: self.command, from: value))
|
||||
otherDisplay.writeDDCValues(command: self.command, value: otherDisplay.convValueToDDC(for: self.command, from: value))
|
||||
}
|
||||
otherDisplay.savePref(value, for: self.command)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,7 +359,7 @@ class DisplaysPrefsCellView: NSTableCellView {
|
|||
self.ddcButton.state = .on
|
||||
self.ddcButtonToggled(self.ddcButton)
|
||||
self.avoidGamma.state = .off
|
||||
self.ddcButtonToggled(self.avoidGamma)
|
||||
self.avoidGamma(self.avoidGamma)
|
||||
self.disableVolumeOSDButton.state = .off
|
||||
self.disableVolumeOSDButton(self.disableVolumeOSDButton)
|
||||
self.pollingModeMenu.selectItem(withTag: 2)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>6851</string>
|
||||
<string>6941</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSBackgroundOnly</key>
|
||||
|
|
|
|||
45
README.md
45
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<div>
|
||||
<h2>MonitorControl - for Apple Silicon and Intel</h2>
|
||||
<p>Controls your external display brightness and volume and shows native OSD.<br/>
|
||||
<p>Controls your external display brightness and volume and shows native OSD.
|
||||
Use menulet sliders or the keyboard, including native Apple keys!</p>
|
||||
<a href="https://github.com/MonitorControl/MonitorControl/releases"><img src=".github/macos_badge_noborder.png" width="175" alt="Download for macOS"/></a>
|
||||
</div>
|
||||
|
|
@ -60,7 +60,7 @@ Go to [Releases](https://github.com/MonitorControl/MonitorControl/releases) and
|
|||
- Modern, stylish and highly customizable menulet reflecting the design of Control Control introduced in Big Sur.
|
||||
- Simple, unobstrusive UI to blend in to the general aesthetics of macOS (even the menu icon can be hidden).
|
||||
- Supports automatic updates for a hassle-free experience.
|
||||
- <b>The best app of its kind, completely FREE (donations accepted) with the source code transparently available!</b>
|
||||
- <b>The best app of its kind, completely FREE ([donations welcome](https://opencollective.com/monitorcontrol)) with the source code transparently available!</b>
|
||||
|
||||
## How to install and use the app
|
||||
|
||||
|
|
@ -76,13 +76,13 @@ Go to [Releases](https://github.com/MonitorControl/MonitorControl/releases) and
|
|||
## Screenshots (Preferences)
|
||||
|
||||
<div align="center">
|
||||
<img src=".github/pref_1.png" width="412" alt="Screenshot"/>
|
||||
<img src=".github/pref_2.png" width="412" alt="Screenshot"/><br/>
|
||||
<img src=".github/pref_3.png" width="412" alt="Screenshot"/>
|
||||
<img src=".github/pref_4.png" width="412" alt="Screenshot"/><br/>
|
||||
<img src=".github/pref_1.png" width="392" alt="Screenshot"/>
|
||||
<img src=".github/pref_2.png" width="392" alt="Screenshot"/>
|
||||
<img src=".github/pref_3.png" width="392" alt="Screenshot"/>
|
||||
<img src=".github/pref_4.png" width="392" alt="Screenshot"/>
|
||||
</div>
|
||||
|
||||
## Compatibility
|
||||
## macOS compatibility
|
||||
|
||||
| MonitorControl version | macOS version |
|
||||
| ---------------------- | ----------------- |
|
||||
|
|
@ -92,25 +92,32 @@ Go to [Releases](https://github.com/MonitorControl/MonitorControl/releases) and
|
|||
|
||||
_* With some limitations - full functionality available on macOS 11 Big Sur or newer._
|
||||
|
||||
Note to f.lux users: the app is now compatible with [f.lux](https://justgetflux.com) as well - please activate `Avoid gamma table manipulation` under `Preferences` » `Displays`! This step is not needed if you use Night Shift.
|
||||
## Supported displays
|
||||
|
||||
## Supported hardware
|
||||
- Most modern LCD displays from all major manufacturers supported implemented DDC/CI protocol via DisplayPort, HDMI, USB-C or VGA to allow for hardware backlight control.
|
||||
- Apple (and LG-Apple) displays and built-in displays are supported using native protocol.
|
||||
- LCD and LED Televisions usually do not implement DDC, these are supported using software alternatives to dim the image (some higher-end sets are able to translate this into hardware backlight dimming).
|
||||
- OLED or mini/micro-LED displays and televisions are fully supported using gamma table manipulation (this is a no-compromise solution for this class of displays).
|
||||
- DisplayLink, Airplay and Sidecar are supported using shade (dark overlay) control.
|
||||
|
||||
* Most modern LCD displays from all major manufacturers supported implemented DDC/CI protocol via DisplayPort, HDMI, USB-C or VGA to allow for hardware backlight control.
|
||||
* Apple (and LG-Apple) displays and built-in displays are supported using native protocol.
|
||||
* LCD and LED Televisions usually do not implement DDC, these are supported using software alternatives to dim the image (some higher-end sets are able to translate this into hardware backlight dimming).
|
||||
* OLED or mini/micro-LED displays and televisions are fully supported using gamma table manipulation (this is a no-compromise solution for this class of displays).
|
||||
* DisplayLink, Airplay and Sidecar are supported using shade (dark overlay) control.
|
||||
Dummy compatibility:
|
||||
|
||||
Notable exceptions for hardware control:
|
||||
- The app is compatible with [BetterDummy](https://github.com/waydabber/BetterDummy) mirrored sets.
|
||||
- The app is compatible with mirrored sets that include a dummy dongle identifying as `28E850`
|
||||
|
||||
* Some displays (notably EIZO) use MCCS over USB or an entirely custom protocol for control. These displays are supported with software dimming only.
|
||||
* The HDMI port of the 2018 Intel Mac mini and 2020 M1 Mac mini prohibit DDC communication. Software control is still available. We recommend connecting the display via the USB-C port (USB-C to HDMI dongles usually work).
|
||||
* DisplayLink docks and dongles do not allow for DDC control on Macs, only software dimming is available for these connections.
|
||||
Notable exceptions for hardware control compatibility:
|
||||
|
||||
- Some displays (notably EIZO) use MCCS over USB or an entirely custom protocol for control. These displays are supported with software dimming only.
|
||||
- The HDMI port of the 2018 Intel Mac mini and 2020 M1 Mac mini prohibit DDC communication. Software control is still available. We recommend connecting the display via the USB-C port (USB-C to HDMI dongles usually work).
|
||||
- DisplayLink docks and dongles do not allow for DDC control on Macs, only software dimming is available for these connections.
|
||||
|
||||
Note to f.lux users - please activate `Avoid gamma table manipulation` under `Preferences` » `Displays`! This step is not needed if you use Night Shift.
|
||||
|
||||
## How to help
|
||||
|
||||
Open [issues](https://github.com/MonitorControl/MonitorControl/issues) if you have a question, an enhancement to suggest or a bug you've found. If you want, you can fork the code yourself and submit a pull request to improve the app.
|
||||
- You can greatly help out [by financing the project with your donation or by being a Sponsor](https://opencollective.com/monitorcontrol)!
|
||||
- Open [issues](https://github.com/MonitorControl/MonitorControl/issues) if you have a question, an enhancement to suggest or a bug you've found.
|
||||
- If you want, you can fork the code yourself and submit a pull request to improve the app (Note: accepting a PR is solely in the collective hands of the maintainers).
|
||||
|
||||
## Localizations
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue