diff --git a/MonitorControl/Model/Display.swift b/MonitorControl/Model/Display.swift index b661249..00f3c56 100644 --- a/MonitorControl/Model/Display.swift +++ b/MonitorControl/Model/Display.swift @@ -165,7 +165,6 @@ class Display: Equatable { _ = self.setDirectBrightness(self.smoothBrightnessTransient, transient: true) self.smoothBrightnessRunning = false } - self.swBrightnessSemaphore.signal() return true } @@ -194,7 +193,10 @@ class Display: Equatable { guard !self.isDummy else { return } - CGGetDisplayTransferByTable(self.identifier, 256, &self.defaultGammaTableRed, &self.defaultGammaTableGreen, &self.defaultGammaTableBlue, &self.defaultGammaTableSampleCount) + guard CGGetDisplayTransferByTable(self.identifier, 256, &self.defaultGammaTableRed, &self.defaultGammaTableGreen, &self.defaultGammaTableBlue, &self.defaultGammaTableSampleCount) == CGError.success else { + self.defaultGammaTablePeak = 1.0 + return + } let redPeak = self.defaultGammaTableRed.max() ?? 0 let greenPeak = self.defaultGammaTableGreen.max() ?? 0 let bluePeak = self.defaultGammaTableBlue.max() ?? 0 @@ -225,23 +227,28 @@ class Display: Equatable { currentValue = self.swBrightnessTransform(value: currentValue) newValue = self.swBrightnessTransform(value: newValue) if smooth { + self.swBrightnessSemaphore.signal() DispatchQueue.global(qos: .userInteractive).async { for transientValue in stride(from: currentValue, to: newValue, by: 0.005 * (currentValue > newValue ? -1 : 1)) { guard app.reconfigureID == 0 else { - self.swBrightnessSemaphore.signal() return } if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) { - _ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier)) + DispatchQueue.main.async { + _ = 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 } let gammaTableBlue = self.defaultGammaTableBlue.map { $0 * transientValue } - CGSetDisplayTransferByTable(self.identifier, self.defaultGammaTableSampleCount, gammaTableRed, gammaTableGreen, gammaTableBlue) + DispatchQueue.main.sync { + CGSetDisplayTransferByTable(self.identifier, self.defaultGammaTableSampleCount, gammaTableRed, gammaTableGreen, gammaTableBlue) + } } Thread.sleep(forTimeInterval: 0.001) // Let's make things quick if not performed in the background } } + return true } else { if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) { self.swBrightnessSemaphore.signal() @@ -273,6 +280,10 @@ class Display: Equatable { self.swBrightnessSemaphore.signal() return self.swBrightnessTransform(value: rawBrightnessValue, reverse: true) } + guard self.defaultGammaTablePeak != 0 else { + self.swBrightnessSemaphore.signal() + return 1.0 + } var gammaTableRed = [CGGammaValue](repeating: 0, count: 256) var gammaTableGreen = [CGGammaValue](repeating: 0, count: 256) var gammaTableBlue = [CGGammaValue](repeating: 0, count: 256) @@ -300,24 +311,26 @@ class Display: Equatable { os_log("Gamma table interference detected, number of events: %{public}@", type: .info, String(DisplayManager.shared.gammaInterferenceCounter)) if DisplayManager.shared.gammaInterferenceCounter >= 3 { DisplayManager.shared.gammaInterferenceWarningShown = true - let alert = NSAlert() - alert.messageText = NSLocalizedString("Is f.lux or similar running?", comment: "Shown in the alert dialog") - alert.informativeText = NSLocalizedString("An other app seems to change the brightness or colors which causes issues.\n\nTo solve this, you need to quit the other app or disable gamma control for your displays in MonitorControl!", comment: "Shown in the alert dialog") - alert.addButton(withTitle: NSLocalizedString("I'll quit the other app", comment: "Shown in the alert dialog")) - alert.addButton(withTitle: NSLocalizedString("Disable gamma control for my displays", comment: "Shown in the alert dialog")) - alert.alertStyle = NSAlert.Style.critical - if alert.runModal() != .alertFirstButtonReturn { - for otherDisplay in DisplayManager.shared.getOtherDisplays() { - _ = otherDisplay.setSwBrightness(1) - _ = otherDisplay.setDirectBrightness(1) - otherDisplay.savePref(true, key: .avoidGamma) - _ = otherDisplay.setSwBrightness(1) - DisplayManager.shared.gammaInterferenceWarningShown = false - DisplayManager.shared.gammaInterferenceCounter = 0 - displaysPrefsVc?.loadDisplayList() + DispatchQueue.main.async { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Is f.lux or similar running?", comment: "Shown in the alert dialog") + alert.informativeText = NSLocalizedString("An other app seems to change the brightness or colors which causes issues.\n\nTo solve this, you need to quit the other app or disable gamma control for your displays in MonitorControl!", comment: "Shown in the alert dialog") + alert.addButton(withTitle: NSLocalizedString("I'll quit the other app", comment: "Shown in the alert dialog")) + alert.addButton(withTitle: NSLocalizedString("Disable gamma control for my displays", comment: "Shown in the alert dialog")) + alert.alertStyle = NSAlert.Style.critical + if alert.runModal() != .alertFirstButtonReturn { + for otherDisplay in DisplayManager.shared.getOtherDisplays() { + _ = otherDisplay.setSwBrightness(1) + _ = otherDisplay.setDirectBrightness(1) + otherDisplay.savePref(true, key: .avoidGamma) + _ = otherDisplay.setSwBrightness(1) + DisplayManager.shared.gammaInterferenceWarningShown = false + DisplayManager.shared.gammaInterferenceCounter = 0 + displaysPrefsVc?.loadDisplayList() + } + } else { + os_log("We won't watch for gamma table interference anymore", type: .info) } - } else { - os_log("We won't watch for gamma table interference anymore", type: .info) } } } diff --git a/MonitorControl/Model/OtherDisplay.swift b/MonitorControl/Model/OtherDisplay.swift index 7c579c4..e98845b 100644 --- a/MonitorControl/Model/OtherDisplay.swift +++ b/MonitorControl/Model/OtherDisplay.swift @@ -97,9 +97,6 @@ class OtherDisplay: Display { case .contrast: currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 0.750) default: currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 1.000) } - if command == .audioSpeakerVolume { - currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 0.125) // lower default audio value as high volume might rattle the user. - } os_log("Setting up display %{public}@ for %{public}@", type: .info, String(self.identifier), String(reflecting: command)) if !self.isSw() { if prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.read.rawValue, self.pollingCount != 0, !app.safeMode { @@ -399,21 +396,32 @@ class OtherDisplay: Display { 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 controlCodes = self.getRemapControlCodes(command: command) if controlCodes.count == 0 { controlCodes.append(command.rawValue) } + var writeAttempted = false + var writeSucceeded = true for controlCode in controlCodes { if Arm64DDC.isArm64 { if self.arm64ddc { - _ = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value) + writeAttempted = true + writeSucceeded = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value) } } else { - _ = self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false + if self.ddc != nil { + writeAttempted = true + writeSucceeded = self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false + } + } + if writeAttempted, !writeSucceeded { + break + } + } + if writeAttempted, writeSucceeded { + self.writeDDCQueue.async(flags: .barrier) { + self.writeDDCLastSavedValue[command] = value + self.savePref(true, key: PrefKey.isTouched, for: command) } } } @@ -487,6 +495,9 @@ class OtherDisplay: Display { let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command)) let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command)) let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command)) + guard maxDDCValue != minDDCValue else { + return UInt16(minDDCValue) + } let curvedValue = pow(max(min(value, 1), 0), curveMultiplier) let deNormalizedValue = (maxDDCValue - minDDCValue) * curvedValue + minDDCValue var intDDCValue = UInt16(min(max(deNormalizedValue, minDDCValue), maxDDCValue)) @@ -500,6 +511,9 @@ class OtherDisplay: Display { let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command)) let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command)) let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command)) + guard maxDDCValue != minDDCValue else { + return 0 + } let normalizedValue = ((min(max(Float(from), minDDCValue), maxDDCValue) - minDDCValue) / (maxDDCValue - minDDCValue)) let deCurvedValue = pow(normalizedValue, 1.0 / curveMultiplier) var value = deCurvedValue diff --git a/MonitorControl/Support/AppDelegate.swift b/MonitorControl/Support/AppDelegate.swift index 6d1013b..9e5b8c4 100644 --- a/MonitorControl/Support/AppDelegate.swift +++ b/MonitorControl/Support/AppDelegate.swift @@ -22,6 +22,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var accessibilityObserver: NSObjectProtocol! var statusItemObserver: NSObjectProtocol! var statusItemVisibilityChangedByUser = true + var configureID: Int = 0 // dispatched configure command ID var reconfigureID: Int = 0 // dispatched reconfigure command ID var sleepID: Int = 0 // sleep event ID var safeMode = false @@ -137,6 +138,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { } os_log("Request for configuration with reconfigreID %{public}@", type: .info, String(dispatchedReconfigureID)) self.reconfigureID = 0 + self.configureID += 1 + let dispatchedConfigureID = self.configureID DisplayManager.shared.gammaInterferenceCounter = 0 DisplayManager.shared.configureDisplays() DisplayManager.shared.addDisplayCounterSuffixes() @@ -144,15 +147,36 @@ class AppDelegate: NSObject, NSApplicationDelegate { if firstrun && prefs.integer(forKey: PrefKey.startupAction.rawValue) != StartupAction.write.rawValue { DisplayManager.shared.resetSwBrightnessForAllDisplays(prefsOnly: true) } - DisplayManager.shared.setupOtherDisplays(firstrun: firstrun) - self.updateMenusAndKeys() - if !firstrun || prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue { - if !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue) { - DisplayManager.shared.restoreSwBrightnessForAllDisplays(async: !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue)) + DispatchQueue.global(qos: .userInitiated).async { + guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else { + return + } + DisplayManager.shared.setupOtherDisplays(firstrun: firstrun) + guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else { + return + } + DispatchQueue.main.async { + guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else { + return + } + self.updateMenusAndKeys() + } + guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else { + return + } + if !firstrun || prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue { + if !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue) { + DisplayManager.shared.restoreSwBrightnessForAllDisplays(async: !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue)) + } + } + DispatchQueue.main.async { + guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else { + return + } + displaysPrefsVc?.loadDisplayList() + self.job(start: true) } } - displaysPrefsVc?.loadDisplayList() - self.job(start: true) } func updateMenusAndKeys() { @@ -298,22 +322,25 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func setStartAtLogin(enabled: Bool) { - let identifier = "\(Bundle.main.bundleIdentifier!)Helper" as CFString - SMLoginItemSetEnabled(identifier, enabled) + let identifier = "\(Bundle.main.bundleIdentifier!)Helper" + if #available(macOS 13.0, *) { + let service = SMAppService.loginItem(identifier: identifier) + do { + if enabled { + try service.register() + } else { + try service.unregister() + } + } catch { + os_log("Unable to update login item registration: %{public}@", type: .error, error.localizedDescription) + } + } else { + SMLoginItemSetEnabled(identifier as CFString, enabled) + } } - func getSystemSettings() -> [String: AnyObject]? { - var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml - let plistPath = NSString(string: "~/Library/Preferences/.GlobalPreferences.plist").expandingTildeInPath - guard let plistXML = FileManager.default.contents(atPath: plistPath) else { - return nil - } - do { - return try PropertyListSerialization.propertyList(from: plistXML, options: .mutableContainersAndLeaves, format: &propertyListFormat) as? [String: AnyObject] - } catch { - os_log("Error reading system prefs plist: %{public}@", type: .info, error.localizedDescription) - return nil - } + func getSystemSettings() -> [String: Any]? { + UserDefaults.standard.persistentDomain(forName: ".GlobalPreferences") } func macOS10() -> Bool { diff --git a/MonitorControl/Support/Arm64DDC.swift b/MonitorControl/Support/Arm64DDC.swift index e8d1495..9ed9969 100644 --- a/MonitorControl/Support/Arm64DDC.swift +++ b/MonitorControl/Support/Arm64DDC.swift @@ -74,6 +74,9 @@ class Arm64DDC: NSObject { var send: [UInt8] = [command] var reply = [UInt8](repeating: 0, count: 11) if Self.performDDCCommunication(service: service, send: &send, reply: &reply, writeSleepTime: writeSleepTime, numOfWriteCycles: numOfWriteCycles, readSleepTime: readSleepTime, numOfRetryAttemps: numOfRetryAttemps, retrySleepTime: retrySleepTime) { + guard reply.count > 9, reply[0] == ARM64_DDC_7BIT_ADDRESS << 1, reply[2] == 0x02, reply[4] == command else { + return nil + } let max = UInt16(reply[6]) * 256 + UInt16(reply[7]) let current = UInt16(reply[8]) * 256 + UInt16(reply[9]) values = (current, max) @@ -97,7 +100,8 @@ class Arm64DDC: NSObject { } var packet: [UInt8] = [UInt8(0x80 | (send.count + 1)), UInt8(send.count)] + send + [0] // Note: the last byte is the place of the checksum, see next line! packet[packet.count - 1] = self.checksum(chk: send.count == 1 ? ARM64_DDC_7BIT_ADDRESS << 1 : ARM64_DDC_7BIT_ADDRESS << 1 ^ dataAddress, data: &packet, start: 0, end: packet.count - 2) - for _ in 1 ... (numOfRetryAttemps ?? 4) + 1 { + let attempts = max(1, Int(min(numOfRetryAttemps ?? 4, UInt8(30)))) + for _ in 0 ..< attempts { for _ in 1 ... max((numOfWriteCycles ?? 2) + 0, 1) { usleep(writeSleepTime ?? 10000) success = IOAVServiceWriteI2C(service, UInt32(ARM64_DDC_7BIT_ADDRESS), UInt32(dataAddress), &packet, UInt32(packet.count)) == 0 @@ -232,7 +236,13 @@ class Arm64DDC: NSObject { static func getIoregServicesForMatching() -> [IOregService] { var serviceLocation = 0 var ioregServicesForMatching: [IOregService] = [] - let ioregRoot: io_registry_entry_t = IORegistryGetRootEntry(kIOMasterPortDefault) + let masterPort: mach_port_t + if #available(macOS 12.0, *) { + masterPort = kIOMainPortDefault + } else { + masterPort = kIOMasterPortDefault + } + let ioregRoot: io_registry_entry_t = IORegistryGetRootEntry(masterPort) defer { IOObjectRelease(ioregRoot) } diff --git a/MonitorControl/Support/DisplayManager.swift b/MonitorControl/Support/DisplayManager.swift index 00a4627..b49e5cf 100644 --- a/MonitorControl/Support/DisplayManager.swift +++ b/MonitorControl/Support/DisplayManager.swift @@ -42,7 +42,6 @@ class DisplayManager { } var shades: [CGDirectDisplayID: NSWindow] = [:] - var shadeGrave: [NSWindow] = [] func isDisqualifiedFromShade(_ displayID: CGDirectDisplayID) -> Bool { if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 { @@ -117,7 +116,6 @@ class DisplayManager { 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 @@ -241,21 +239,6 @@ class DisplayManager { self.displays.compactMap { $0 as? OtherDisplay } } - func sortDisplays() { - // Opsiyonel: sıralamadan önce log al - let before = displays.map { $0.name } - os_log("Displays before sorting: %{public}@", before) - - // In‑place sıralama - displays.sort { lhs, rhs in - lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending - } - - // Opsiyonel: sıralamadan sonra log al - let after = displays.map { $0.name } - os_log("Displays after sorting: %{public}@", after) - } - func sortDisplaysByFriendlyName() -> [Display] { return displays.sorted { lhs, rhs in let lhsTitle = lhs.readPrefAsString(key: .friendlyName).isEmpty @@ -337,11 +320,14 @@ class DisplayManager { if Arm64DDC.isArm64 { os_log("arm64 AVService update requested", type: .info) var displayIDs: [CGDirectDisplayID] = [] - for otherDisplay in self.getOtherDisplays() { + let otherDisplays = self.getOtherDisplays() + for otherDisplay in otherDisplays { + otherDisplay.arm64avService = nil + otherDisplay.arm64ddc = false displayIDs.append(otherDisplay.identifier) } for serviceMatch in Arm64DDC.getServiceMatches(displayIDs: displayIDs) { - for otherDisplay in self.getOtherDisplays() where otherDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil { + for otherDisplay in otherDisplays 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 { diff --git a/MonitorControl/Support/MenuHandler.swift b/MonitorControl/Support/MenuHandler.swift index 6fcd486..9cb4aff 100644 --- a/MonitorControl/Support/MenuHandler.swift +++ b/MonitorControl/Support/MenuHandler.swift @@ -56,11 +56,12 @@ class MenuHandler: NSMenu, NSMenuDelegate { displays = DisplayManager.shared.sortDisplaysByFriendlyName() let relevant = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.relevant.rawValue let combine = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.combine.rawValue + let currentDisplayID = currentDisplay.map { DisplayManager.resolveEffectiveDisplayID($0.identifier) } 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 || DisplayManager.resolveEffectiveDisplayID(display.identifier) == DisplayManager.resolveEffectiveDisplayID(currentDisplay!.identifier)) && !display.isDummy { + for display in displays where (!relevant || currentDisplayID.map { DisplayManager.resolveEffectiveDisplayID(display.identifier) == $0 } ?? false) && !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 de28e90..2c6fc29 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -320,9 +320,7 @@ class SliderHandler { slider.floatValue = value } } - if self.percentageBox == self.percentageBox { - self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" - } + self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" for display in self.displays { slider.setHighlightItem(display.identifier, value: value) if self.command == .brightness, let appleDisplay = display as? AppleDisplay { @@ -378,9 +376,7 @@ class SliderHandler { } else { slider.setDisplayHighlightItems(false) } - if self.percentageBox == self.percentageBox { - self.percentageBox?.stringValue = "\(String(format: "%.0f%%", Double(value) * 100))" - } + self.percentageBox?.stringValue = "\(String(format: "%.0f%%", Double(value) * 100))" } } }