diff --git a/.swiftlint.yml b/.swiftlint.yml index 9252648..7cc44de 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -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: diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj index cdc9cba..95fc229 100644 --- a/MonitorControl.xcodeproj/project.pbxproj +++ b/MonitorControl.xcodeproj/project.pbxproj @@ -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)"; diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 1c3560c..1b6ff21 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 6851 + 6941 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Model/Display.swift b/MonitorControl/Model/Display.swift index b68e518..0563b50 100644 --- a/MonitorControl/Model/Display.swift +++ b/MonitorControl/Model/Display.swift @@ -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) } diff --git a/MonitorControl/Model/OtherDisplay.swift b/MonitorControl/Model/OtherDisplay.swift index f95afcc..9c95cb5 100644 --- a/MonitorControl/Model/OtherDisplay.swift +++ b/MonitorControl/Model/OtherDisplay.swift @@ -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) } } diff --git a/MonitorControl/Support/DisplayManager.swift b/MonitorControl/Support/DisplayManager.swift index c98c476..f426972 100644 --- a/MonitorControl/Support/DisplayManager.swift +++ b/MonitorControl/Support/DisplayManager.swift @@ -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 } diff --git a/MonitorControl/Support/MenuHandler.swift b/MonitorControl/Support/MenuHandler.swift index 2a61c19..34615a7 100644 --- a/MonitorControl/Support/MenuHandler.swift +++ b/MonitorControl/Support/MenuHandler.swift @@ -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) diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index 51fcb09..a1c2cc4 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -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) } diff --git a/MonitorControl/View Controllers/DisplaysPrefsCellView.swift b/MonitorControl/View Controllers/DisplaysPrefsCellView.swift index 7bc4e8d..ccb0d9e 100644 --- a/MonitorControl/View Controllers/DisplaysPrefsCellView.swift +++ b/MonitorControl/View Controllers/DisplaysPrefsCellView.swift @@ -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) diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index a30473e..955c2f9 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 6851 + 6941 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly diff --git a/README.md b/README.md index 8079360..f7d3e01 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

MonitorControl - for Apple Silicon and Intel

-

Controls your external display brightness and volume and shows native OSD.
+

Controls your external display brightness and volume and shows native OSD. Use menulet sliders or the keyboard, including native Apple keys!

Download for macOS
@@ -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. -- The best app of its kind, completely FREE (donations accepted) with the source code transparently available! +- The best app of its kind, completely FREE ([donations welcome](https://opencollective.com/monitorcontrol)) with the source code transparently available! ## How to install and use the app @@ -76,13 +76,13 @@ Go to [Releases](https://github.com/MonitorControl/MonitorControl/releases) and ## Screenshots (Preferences)
-Screenshot -Screenshot
-Screenshot -Screenshot
+Screenshot +Screenshot +Screenshot +Screenshot
-## 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