diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj index bb208a2..3d7bd08 100644 --- a/MonitorControl.xcodeproj/project.pbxproj +++ b/MonitorControl.xcodeproj/project.pbxproj @@ -8,14 +8,11 @@ /* Begin PBXBuildFile section */ 2894D9B82280B30500DF58DA /* CGDirectDisplayID+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2894D9B72280B30500DF58DA /* CGDirectDisplayID+Extension.swift */; }; - 28D1DDF0227FBD99004CB494 /* EDID+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D1DDEC227FB8F2004CB494 /* EDID+Extension.swift */; }; 28D1DDF2227FBE71004CB494 /* NSScreen+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D1DDF1227FBE71004CB494 /* NSScreen+Extension.swift */; }; 28D1DDF3227FC8C6004CB494 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB01D9A4016007BCDC5 /* Assets.xcassets */; }; 56754EAF1D9A4016007BCDC5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */; }; 56754EB11D9A4016007BCDC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB01D9A4016007BCDC5 /* Assets.xcassets */; }; 6C20466C23153E4F00859767 /* Display+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C20466B23153E4F00859767 /* Display+Extension.swift */; }; - 6C2EA1CD228F644B00060E3F /* OnlyIntegerValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2EA1CC228F644B00060E3F /* OnlyIntegerValueFormatter.swift */; }; - 6C2EA1CF228F7DFB00060E3F /* PollingMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2EA1CE228F7DFB00060E3F /* PollingMode.swift */; }; 6C85EFDA22C941B000227EA1 /* DisplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85EFD922C941B000227EA1 /* DisplayManager.swift */; }; 6C85EFE122CC00AD00227EA1 /* NSNotification+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85EFE022CC00AD00227EA1 /* NSNotification+Extension.swift */; }; 6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBFE27923DB266000D1BC41 /* Display.swift */; }; @@ -23,13 +20,14 @@ 6CC260F6256AD8F900613714 /* Preferences+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC260F5256AD8F900613714 /* Preferences+Extension.swift */; }; 6CD35F53264FFFC6001F1344 /* SimplyCoreAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD35F52264FFFC6001F1344 /* SimplyCoreAudio */; }; 6CD35F5626500008001F1344 /* MediaKeyTap in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD35F5526500008001F1344 /* MediaKeyTap */; }; - 6CD35F592650002E001F1344 /* DDC in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD35F582650002E001F1344 /* DDC */; }; 6CD35F5C2650003F001F1344 /* Preferences in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD35F5B2650003F001F1344 /* Preferences */; }; 6CDA0FCF26485A8300F52125 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6CDA0FCD26485A8300F52125 /* Main.storyboard */; }; AA062E8A26C9A039007E628C /* DisplaysPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA062E8926C9A039007E628C /* DisplaysPrefsViewController.swift */; }; AA062E8E26CA7BE5007E628C /* DisplaysPrefsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA062E8D26CA7BE5007E628C /* DisplaysPrefsCellView.swift */; }; - AA16139B26BE772E00DCF027 /* Arm64DDCUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA16139A26BE772E00DCF027 /* Arm64DDCUtils.swift */; }; + AA16139B26BE772E00DCF027 /* Arm64DDC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA16139A26BE772E00DCF027 /* Arm64DDC.swift */; }; AA3B4A2826AE103C00B74CD2 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA3B4A2726AE103C00B74CD2 /* README.md */; }; + AA4398A926DD55DA00943F16 /* IntelDDC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4398A826DD55DA00943F16 /* IntelDDC.swift */; }; + AA473EB126DFF8DE0063A181 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA473EB026DFF8DE0063A181 /* Command.swift */; }; AA665A5D26C5892800FEF2C1 /* AboutPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA665A5C26C5892800FEF2C1 /* AboutPrefsViewController.swift */; }; AA9AE86F26B5BF3D00B6CA65 /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA9AE86E26B5BF3D00B6CA65 /* OSD.framework */; }; AA9AE87126B5BFB700B6CA65 /* CoreDisplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA9AE87026B5BFB700B6CA65 /* CoreDisplay.framework */; }; @@ -63,7 +61,6 @@ 1E7ECF3F22A4552400E4E701 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 1E7ECF4122A4553000E4E701 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/MainMenu.strings; sourceTree = ""; }; 2894D9B72280B30500DF58DA /* CGDirectDisplayID+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGDirectDisplayID+Extension.swift"; sourceTree = ""; }; - 28D1DDEC227FB8F2004CB494 /* EDID+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EDID+Extension.swift"; sourceTree = ""; }; 28D1DDF1227FBE71004CB494 /* NSScreen+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+Extension.swift"; sourceTree = ""; }; 2EAA5B7E24BF9E9A00937821 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/MainMenu.strings; sourceTree = ""; }; 2EAA5B7F24BF9E9A00937821 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -78,8 +75,6 @@ 56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; }; 56754EB01D9A4016007BCDC5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6C20466B23153E4F00859767 /* Display+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Display+Extension.swift"; sourceTree = ""; }; - 6C2EA1CC228F644B00060E3F /* OnlyIntegerValueFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlyIntegerValueFormatter.swift; sourceTree = ""; }; - 6C2EA1CE228F7DFB00060E3F /* PollingMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollingMode.swift; sourceTree = ""; }; 6C85EFD922C941B000227EA1 /* DisplayManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayManager.swift; sourceTree = ""; }; 6C85EFE022CC00AD00227EA1 /* NSNotification+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSNotification+Extension.swift"; sourceTree = ""; }; 6CAD134F23624CC1009BD53F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/MainMenu.strings; sourceTree = ""; }; @@ -102,8 +97,10 @@ 6CDA0FD826485AAE00F52125 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Main.strings; sourceTree = ""; }; AA062E8926C9A039007E628C /* DisplaysPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaysPrefsViewController.swift; sourceTree = ""; }; AA062E8D26CA7BE5007E628C /* DisplaysPrefsCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaysPrefsCellView.swift; sourceTree = ""; }; - AA16139A26BE772E00DCF027 /* Arm64DDCUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Arm64DDCUtils.swift; sourceTree = ""; }; + AA16139A26BE772E00DCF027 /* Arm64DDC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Arm64DDC.swift; sourceTree = ""; }; AA3B4A2726AE103C00B74CD2 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + AA4398A826DD55DA00943F16 /* IntelDDC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntelDDC.swift; sourceTree = ""; }; + AA473EB026DFF8DE0063A181 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; AA665A5C26C5892800FEF2C1 /* AboutPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutPrefsViewController.swift; sourceTree = ""; }; AA6686F026B8172E00AF74A2 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Main.strings; sourceTree = ""; }; AA6686F126B8172E00AF74A2 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/MainMenu.strings; sourceTree = ""; }; @@ -147,7 +144,6 @@ AA9AE87126B5BFB700B6CA65 /* CoreDisplay.framework in Frameworks */, 6CD35F53264FFFC6001F1344 /* SimplyCoreAudio in Frameworks */, 6CD35F5626500008001F1344 /* MediaKeyTap in Frameworks */, - 6CD35F592650002E001F1344 /* DDC in Frameworks */, AA9AE86F26B5BF3D00B6CA65 /* OSD.framework in Frameworks */, 6CD35F5C2650003F001F1344 /* Preferences in Frameworks */, AADB625A26BC196900DFFAA5 /* DisplayServices.framework in Frameworks */, @@ -178,7 +174,6 @@ isa = PBXGroup; children = ( 2894D9B72280B30500DF58DA /* CGDirectDisplayID+Extension.swift */, - 28D1DDEC227FB8F2004CB494 /* EDID+Extension.swift */, 28D1DDF1227FBE71004CB494 /* NSScreen+Extension.swift */, 6C85EFE022CC00AD00227EA1 /* NSNotification+Extension.swift */, 6C20466B23153E4F00859767 /* Display+Extension.swift */, @@ -248,10 +243,10 @@ F01B0685228221B6008E64DB /* Bridging-Header.h */, F01B0680228221B6008E64DB /* Localizable.strings */, F01B0683228221B6008E64DB /* Utils.swift */, - 6C2EA1CC228F644B00060E3F /* OnlyIntegerValueFormatter.swift */, - 6C2EA1CE228F7DFB00060E3F /* PollingMode.swift */, FE4E0895249D584C003A50BB /* OSDUtils.swift */, - AA16139A26BE772E00DCF027 /* Arm64DDCUtils.swift */, + AA16139A26BE772E00DCF027 /* Arm64DDC.swift */, + AA4398A826DD55DA00943F16 /* IntelDDC.swift */, + AA473EB026DFF8DE0063A181 /* Command.swift */, ); path = Support; sourceTree = ""; @@ -310,7 +305,6 @@ packageProductDependencies = ( 6CD35F52264FFFC6001F1344 /* SimplyCoreAudio */, 6CD35F5526500008001F1344 /* MediaKeyTap */, - 6CD35F582650002E001F1344 /* DDC */, 6CD35F5B2650003F001F1344 /* Preferences */, ); productName = MonitorControl.OSX; @@ -383,7 +377,6 @@ packageReferences = ( 6CD35F51264FFFC6001F1344 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */, 6CD35F5426500008001F1344 /* XCRemoteSwiftPackageReference "MediaKeyTap" */, - 6CD35F572650002E001F1344 /* XCRemoteSwiftPackageReference "DDC" */, 6CD35F5A2650003F001F1344 /* XCRemoteSwiftPackageReference "Preferences" */, ); productRefGroup = 56754EAC1D9A4016007BCDC5 /* Products */; @@ -494,7 +487,7 @@ }; F03A8DF01FFB9D4C0034DC27 /* [Lint] Run SwiftLint */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 12; files = ( ); inputPaths = ( @@ -516,22 +509,21 @@ 56754EAF1D9A4016007BCDC5 /* AppDelegate.swift in Sources */, 6C85EFE122CC00AD00227EA1 /* NSNotification+Extension.swift in Sources */, AA062E8A26C9A039007E628C /* DisplaysPrefsViewController.swift in Sources */, - 6C2EA1CD228F644B00060E3F /* OnlyIntegerValueFormatter.swift in Sources */, + AA473EB126DFF8DE0063A181 /* Command.swift in Sources */, 2894D9B82280B30500DF58DA /* CGDirectDisplayID+Extension.swift in Sources */, 6CC260F6256AD8F900613714 /* Preferences+Extension.swift in Sources */, AA665A5D26C5892800FEF2C1 /* AboutPrefsViewController.swift in Sources */, - 6C2EA1CF228F7DFB00060E3F /* PollingMode.swift in Sources */, 6CBFE27C23DB27A200D1BC41 /* InternalDisplay.swift in Sources */, AA062E8E26CA7BE5007E628C /* DisplaysPrefsCellView.swift in Sources */, FE4E0896249D584C003A50BB /* OSDUtils.swift in Sources */, 6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */, F03A8DF21FFBAA6F0034DC27 /* ExternalDisplay.swift in Sources */, - 28D1DDF0227FBD99004CB494 /* EDID+Extension.swift in Sources */, 6C20466C23153E4F00859767 /* Display+Extension.swift in Sources */, - AA16139B26BE772E00DCF027 /* Arm64DDCUtils.swift in Sources */, + AA16139B26BE772E00DCF027 /* Arm64DDC.swift in Sources */, F0445D3820023E710025AE82 /* MainPrefsViewController.swift in Sources */, 28D1DDF2227FBE71004CB494 /* NSScreen+Extension.swift in Sources */, F01B069F228221B7008E64DB /* SliderHandler.swift in Sources */, + AA4398A926DD55DA00943F16 /* IntelDDC.swift in Sources */, 6C85EFDA22C941B000227EA1 /* DisplayManager.swift in Sources */, F01B069A228221B7008E64DB /* Utils.swift in Sources */, ); @@ -756,6 +748,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl/Support/Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -790,6 +783,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl/Support/Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -905,14 +899,6 @@ kind = branch; }; }; - 6CD35F572650002E001F1344 /* XCRemoteSwiftPackageReference "DDC" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/reitermarkus/DDC.swift"; - requirement = { - branch = master; - kind = branch; - }; - }; 6CD35F5A2650003F001F1344 /* XCRemoteSwiftPackageReference "Preferences" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/Preferences"; @@ -934,11 +920,6 @@ package = 6CD35F5426500008001F1344 /* XCRemoteSwiftPackageReference "MediaKeyTap" */; productName = MediaKeyTap; }; - 6CD35F582650002E001F1344 /* DDC */ = { - isa = XCSwiftPackageProductDependency; - package = 6CD35F572650002E001F1344 /* XCRemoteSwiftPackageReference "DDC" */; - productName = DDC; - }; 6CD35F5B2650003F001F1344 /* Preferences */ = { isa = XCSwiftPackageProductDependency; package = 6CD35F5A2650003F001F1344 /* XCRemoteSwiftPackageReference "Preferences" */; diff --git a/MonitorControl/AppDelegate.swift b/MonitorControl/AppDelegate.swift index 690c6b1..7b97232 100644 --- a/MonitorControl/AppDelegate.swift +++ b/MonitorControl/AppDelegate.swift @@ -19,6 +19,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var accessibilityObserver: NSObjectProtocol! var reconfigureID: Int = 0 // dispatched reconfigure command ID var sleepID: Int = 0 // Don't reconfigure display as the system or display is sleeping or wake just recently. + var safeMode = false // Safe mode engaged during startup? let debugSw: Bool = false lazy var preferencesWindowController: PreferencesWindowController = { let storyboard = NSStoryboard(name: "Main", bundle: Bundle.main) @@ -38,6 +39,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_: Notification) { app = self self.subscribeEventListeners() + if NSEvent.modifierFlags.contains(NSEvent.ModifierFlags.shift) { + self.safeMode = true + self.handlePreferenceReset() + Utils.alert(text: NSLocalizedString("Safe Mode Activated", comment: "Shown in the alert dialog"), info: NSLocalizedString("Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked.", comment: "Shown in the alert dialog")) + } self.setDefaultPrefs() if #available(macOS 11.0, *) { self.statusItem.button?.image = NSImage(systemSymbolName: "sun.max", accessibilityDescription: "MonitorControl") @@ -88,7 +94,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { for i in 0 ..< self.statusMenu.items.count - 2 { items.append(self.statusMenu.items[i]) } - for item in items { self.statusMenu.removeItem(item) } @@ -97,13 +102,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func updateArm64AVServices() { - if Arm64DDCUtils.isArm64 { + if Arm64DDC.isArm64 { os_log("arm64 AVService update requested", type: .info) var displayIDs: [CGDirectDisplayID] = [] for externalDisplay in DisplayManager.shared.getExternalDisplays() { displayIDs.append(externalDisplay.identifier) } - for serviceMatch in Arm64DDCUtils.getServiceMatches(displayIDs: displayIDs) { + for serviceMatch in Arm64DDC.getServiceMatches(displayIDs: displayIDs) { for externalDisplay in DisplayManager.shared.getExternalDisplays() where externalDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil { externalDisplay.arm64avService = serviceMatch.service os_log("Display service match successful for display %{public}@", type: .info, String(serviceMatch.displayID)) @@ -213,11 +218,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { if !display.isSw() { if prefs.bool(forKey: Utils.PrefKeys.showVolume.rawValue) { - let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, forDisplay: display, command: .audioSpeakerVolume, title: NSLocalizedString("Volume", comment: "Shown in menu")) + let volumeSliderHandler = SliderHandler.addSliderMenuItem(toMenu: monitorSubMenu, forDisplay: display, command: .audioSpeakerVolume, title: NSLocalizedString("Volume", comment: "Shown in menu")) display.volumeSliderHandler = volumeSliderHandler } if prefs.bool(forKey: Utils.PrefKeys.showContrast.rawValue) { - let contrastSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, forDisplay: display, command: .contrast, title: NSLocalizedString("Contrast", comment: "Shown in menu")) + let contrastSliderHandler = SliderHandler.addSliderMenuItem(toMenu: monitorSubMenu, forDisplay: display, command: .contrast, title: NSLocalizedString("Contrast", comment: "Shown in menu")) display.contrastSliderHandler = contrastSliderHandler } } @@ -225,7 +230,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { if !display.isSw(), prefs.bool(forKey: Utils.PrefKeys.lowerSwAfterBrightness.rawValue) { numOfTickMarks = 0 // 1 - I disabled this because tickmarks are buggy in dark mode on Monterey (probably Big Sur as well). } - let brightnessSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, forDisplay: display, command: .brightness, title: NSLocalizedString("Brightness", comment: "Shown in menu"), numOfTickMarks: numOfTickMarks) + let brightnessSliderHandler = SliderHandler.addSliderMenuItem(toMenu: monitorSubMenu, forDisplay: display, command: .brightness, title: NSLocalizedString("Brightness", comment: "Shown in menu"), numOfTickMarks: numOfTickMarks) display.brightnessSliderHandler = brightnessSliderHandler let monitorMenuItem = NSMenuItem() @@ -258,10 +263,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.wakeNotofication), name: NSWorkspace.screensDidWakeNotification, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.sleepNotification), name: NSWorkspace.willSleepNotification, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.wakeNotofication), name: NSWorkspace.didWakeNotification, object: nil) - _ = DistributedNotificationCenter.default().addObserver(forName: .accessibilityApi, object: nil, queue: nil) { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.updateMediaKeyTap() // listen for accessibility status changes - } - } + _ = DistributedNotificationCenter.default().addObserver(forName: .accessibilityApi, object: nil, queue: nil) { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.updateMediaKeyTap() } } // listen for accessibility status changes } @objc private func sleepNotification() { @@ -287,6 +289,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { let dispatchedReconfigureID = self.reconfigureID os_log("Display needs reconfig after sober with reconfigureID %{public}@", type: .info, String(dispatchedReconfigureID)) self.updateDisplays(dispatchedReconfigureID: dispatchedReconfigureID) + } else if Arm64DDC.isArm64 { + os_log("Displays don't need reconfig after sober but might need AVServices update", type: .info) + self.updateArm64AVServices() } } } @@ -335,20 +340,21 @@ extension AppDelegate: MediaKeyTapDelegate { } return } - if self.handleOpenPrefPane(mediaKey: mediaKey, event: event, modifiers: modifiers) { + let isPressed = event?.keyPressed ?? true + let isRepeat = event?.keyRepeat ?? false + if isPressed, self.handleOpenPrefPane(mediaKey: mediaKey, event: event, modifiers: modifiers) { return } let isSmallIncrement = modifiers?.isSuperset(of: NSEvent.ModifierFlags([.shift, .option])) ?? false // control internal display when holding ctrl modifier let isControlModifier = modifiers?.isSuperset(of: NSEvent.ModifierFlags([.control])) ?? false if isControlModifier, mediaKey == .brightnessUp || mediaKey == .brightnessDown { - if let internalDisplay = DisplayManager.shared.getBuiltInDisplay() as? InternalDisplay { + if isPressed, let internalDisplay = DisplayManager.shared.getBuiltInDisplay() as? InternalDisplay { internalDisplay.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement) return } } let oppositeKey: MediaKey? = self.oppositeMediaKey(mediaKey: mediaKey) - let isRepeat = event?.keyRepeat ?? false // If the opposite key to the one being held has an active timer, cancel it - we'll be going in the opposite direction if let oppositeKey = oppositeKey, let oppositeKeyTimer = self.keyRepeatTimers[oppositeKey], oppositeKeyTimer.isValid { oppositeKeyTimer.invalidate() @@ -359,50 +365,31 @@ extension AppDelegate: MediaKeyTapDelegate { } mediaKeyTimer.invalidate() } - self.sendDisplayCommand(mediaKey: mediaKey, isRepeat: isRepeat, isSmallIncrement: isSmallIncrement) + self.sendDisplayCommand(mediaKey: mediaKey, isRepeat: isRepeat, isSmallIncrement: isSmallIncrement, isPressed: isPressed) } - private func getAffectedDisplays() -> [Display]? { - var affectedDisplays: [Display] - let allDisplays = DisplayManager.shared.getAllNonVirtualDisplays() - guard let currentDisplay = DisplayManager.shared.getCurrentDisplay() else { - return nil - } - // let allDisplays = prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) ? displays : [currentDisplay] - if prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) { - affectedDisplays = allDisplays - } else { - affectedDisplays = [currentDisplay] - if CGDisplayIsInHWMirrorSet(currentDisplay.identifier) != 0 || CGDisplayIsInMirrorSet(currentDisplay.identifier) != 0, CGDisplayMirrorsDisplay(currentDisplay.identifier) == 0 { - for display in allDisplays where CGDisplayMirrorsDisplay(display.identifier) == currentDisplay.identifier { - affectedDisplays.append(display) - } - } - } - return affectedDisplays - } - - private func sendDisplayCommand(mediaKey: MediaKey, isRepeat: Bool, isSmallIncrement: Bool) { - guard self.sleepID == 0, self.reconfigureID == 0, let affectedDisplays = self.getAffectedDisplays() else { + private func sendDisplayCommand(mediaKey: MediaKey, isRepeat: Bool, isSmallIncrement: Bool, isPressed: Bool) { + guard self.sleepID == 0, self.reconfigureID == 0, let affectedDisplays = DisplayManager.shared.getAffectedDisplays() else { return } - var isAnyDisplayInSwAfterBrightnessMode: Bool = false - for display in affectedDisplays where ((display as? ExternalDisplay)?.isSwBrightnessNotDefault() ?? false) && !((display as? ExternalDisplay)?.isSw() ?? false) { - isAnyDisplayInSwAfterBrightnessMode = true - } - // let delay = isRepeat ? 0.05 : 0 // Introduce a small delay to handle the media key being held down - Update: it is not clear why this is needed but it blocks the media keys working when the menu is open and also it doesn't seem to affect external bluetooth keyboards but slows down internal keyboards for some reason. Things seem to work better this being disabled. - // self.keyRepeatTimers[mediaKey] = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in + var wasNotIsPressedVolumeSentAlready = false for display in affectedDisplays where display.isEnabled && !display.isVirtual { switch mediaKey { case .brightnessUp: - if !(isAnyDisplayInSwAfterBrightnessMode && !(((display as? ExternalDisplay)?.isSwBrightnessNotDefault() ?? false) && !((display as? ExternalDisplay)?.isSw() ?? false))) { + var isAnyDisplayInSwAfterBrightnessMode: Bool = false + for display in affectedDisplays where ((display as? ExternalDisplay)?.isSwBrightnessNotDefault() ?? false) && !((display as? ExternalDisplay)?.isSw() ?? false) { + isAnyDisplayInSwAfterBrightnessMode = true + } + if isPressed, !(isAnyDisplayInSwAfterBrightnessMode && !(((display as? ExternalDisplay)?.isSwBrightnessNotDefault() ?? false) && !((display as? ExternalDisplay)?.isSw() ?? false))) { display.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement) } case .brightnessDown: - display.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement) + if isPressed { + display.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement) + } case .mute: - // The mute key should not respond to press + hold - if !isRepeat { + // The mute key should not respond to press + hold or keyup + if !isRepeat, isPressed { // mute only matters for external displays if let display = display as? ExternalDisplay { display.toggleMute() @@ -411,13 +398,15 @@ extension AppDelegate: MediaKeyTapDelegate { case .volumeUp, .volumeDown: // volume only matters for external displays if let display = display as? ExternalDisplay { - display.stepVolume(isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement) + if isPressed || !wasNotIsPressedVolumeSentAlready { + display.stepVolume(isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement, isPressed: isPressed) + } + wasNotIsPressedVolumeSentAlready = true } default: return } } - // }) } @objc func handleListenForChanged() { @@ -473,7 +462,7 @@ extension AppDelegate: MediaKeyTapDelegate { self.mediaKeyTap?.stop() // returning an empty array listens for all mediakeys in MediaKeyTap if keys.count > 0 { - self.mediaKeyTap = MediaKeyTap(delegate: self, for: keys, observeBuiltIn: true) + self.mediaKeyTap = MediaKeyTap(delegate: self, on: KeyPressMode.keyDownAndUp, for: keys, observeBuiltIn: true) self.mediaKeyTap?.start() } } diff --git a/MonitorControl/Extensions/EDID+Extension.swift b/MonitorControl/Extensions/EDID+Extension.swift deleted file mode 100644 index 53e078c..0000000 --- a/MonitorControl/Extensions/EDID+Extension.swift +++ /dev/null @@ -1,33 +0,0 @@ -import DDC - -public extension EDID { - func displayName() -> String? { - let descriptors = [self.descriptors.0, self.descriptors.1, self.descriptors.2, self.descriptors.3] - - for descriptor in descriptors { - switch descriptor { - case let .displayName(name): - return name - default: - continue - } - } - - return nil - } - - func serialNumber() -> String? { - let descriptors = [self.descriptors.0, self.descriptors.1, self.descriptors.2, self.descriptors.3] - - for descriptor in descriptors { - switch descriptor { - case let .serialNumber(number): - return number - default: - continue - } - } - - return String(self.serialNumber) - } -} diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index d55c75f..993e52d 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 2886 + 3094 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Manager/DisplayManager.swift b/MonitorControl/Manager/DisplayManager.swift index 0eccce1..5981676 100644 --- a/MonitorControl/Manager/DisplayManager.swift +++ b/MonitorControl/Manager/DisplayManager.swift @@ -1,5 +1,4 @@ import Cocoa -import DDC class DisplayManager { public static let shared = DisplayManager() @@ -115,7 +114,7 @@ class DisplayManager { if savedPrefValue < externalDisplay.getSwMaxBrightness() { self.setBrightnessSliderValue(externalDisplay: externalDisplay, value: Int32(Float(sliderMax / 2) * (Float(savedPrefValue) / Float(externalDisplay.getSwMaxBrightness())))) } else { - self.setBrightnessSliderValue(externalDisplay: externalDisplay, value: Int32(sliderMax / 2) + Int32(externalDisplay.getValue(for: DDC.Command.brightness))) + self.setBrightnessSliderValue(externalDisplay: externalDisplay, value: Int32(sliderMax / 2) + Int32(externalDisplay.getValue(for: .brightness))) } } else if externalDisplay.isSw() { self.setBrightnessSliderValue(externalDisplay: externalDisplay, value: Int32(Float(sliderMax) * (Float(savedPrefValue) / Float(externalDisplay.getSwMaxBrightness())))) @@ -151,4 +150,24 @@ class DisplayManager { } return defaultName } + + func getAffectedDisplays() -> [Display]? { + var affectedDisplays: [Display] + let allDisplays = DisplayManager.shared.getAllNonVirtualDisplays() + guard let currentDisplay = DisplayManager.shared.getCurrentDisplay() else { + return nil + } + // let allDisplays = prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) ? displays : [currentDisplay] + if prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) { + affectedDisplays = allDisplays + } else { + affectedDisplays = [currentDisplay] + if CGDisplayIsInHWMirrorSet(currentDisplay.identifier) != 0 || CGDisplayIsInMirrorSet(currentDisplay.identifier) != 0, CGDisplayMirrorsDisplay(currentDisplay.identifier) == 0 { + for display in allDisplays where CGDisplayMirrorsDisplay(display.identifier) == currentDisplay.identifier { + affectedDisplays.append(display) + } + } + } + return affectedDisplays + } } diff --git a/MonitorControl/Model/Display.swift b/MonitorControl/Model/Display.swift index c193980..26df7db 100644 --- a/MonitorControl/Model/Display.swift +++ b/MonitorControl/Model/Display.swift @@ -6,7 +6,6 @@ // Copyright © 2020 MonitorControl. All rights reserved. // -import DDC import Foundation import os.log @@ -180,7 +179,7 @@ class Display { return false } - func showOsd(command: DDC.Command, value: Int, maxValue: Int = 100, roundChiclet: Bool = false, lock: Bool = false) { + func showOsd(command: Command, value: Int, maxValue: Int = 100, roundChiclet: Bool = false, lock: Bool = false) { guard let manager = OSDManager.sharedManager() as? OSDManager else { return } diff --git a/MonitorControl/Model/ExternalDisplay.swift b/MonitorControl/Model/ExternalDisplay.swift index 78e24c8..6f9dd82 100644 --- a/MonitorControl/Model/ExternalDisplay.swift +++ b/MonitorControl/Model/ExternalDisplay.swift @@ -1,6 +1,5 @@ import AVFoundation import Cocoa -import DDC import IOKit import os.log @@ -8,12 +7,14 @@ class ExternalDisplay: Display { var brightnessSliderHandler: SliderHandler? var volumeSliderHandler: SliderHandler? var contrastSliderHandler: SliderHandler? - var ddc: DDC? + var ddc: IntelDDC? var arm64ddc: Bool = false var arm64avService: IOAVService? let DDC_HARD_MAX_LIMIT: Int = 100 + let ddcQueue = DispatchQueue(label: "DDC queue") + private let prefs = UserDefaults.standard var enableMuteUnmute: Bool { @@ -46,13 +47,11 @@ class ExternalDisplay: Display { } } - private var audioPlayer: AVAudioPlayer? - override init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?, isVirtual: Bool = false) { super.init(identifier, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, isVirtual: isVirtual) - if !isVirtual, !Arm64DDCUtils.isArm64 { - self.ddc = DDC(for: identifier) + if !isVirtual, !Arm64DDC.isArm64 { + self.ddc = IntelDDC(for: identifier) } } @@ -81,10 +80,6 @@ class ExternalDisplay: Display { let volumeDDCValue = UInt16(volumeOSDValue) - guard self.writeDDCValues(command: .audioSpeakerVolume, value: volumeDDCValue) == true else { - return - } - if self.enableMuteUnmute { guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else { return @@ -93,6 +88,10 @@ class ExternalDisplay: Display { self.saveValue(muteValue, for: .audioMuteScreenBlank) + if !self.enableMuteUnmute || volumeOSDValue > 0 { + _ = self.writeDDCValues(command: .audioSpeakerVolume, value: volumeDDCValue) + } + if !fromVolumeSlider { if !self.hideOsd { self.showOsd(command: volumeOSDValue > 0 ? .audioSpeakerVolume : .audioMuteScreenBlank, value: volumeOSDValue, roundChiclet: true) @@ -108,9 +107,13 @@ class ExternalDisplay: Display { } } - func stepVolume(isUp: Bool, isSmallIncrement: Bool) { - var muteValue: Int? + func stepVolume(isUp: Bool, isSmallIncrement: Bool, isPressed: Bool) { let currentValue = self.getValue(for: .audioSpeakerVolume) + guard isPressed else { + self.playVolumeChangedSound() + return + } + var muteValue: Int? let maxValue = self.getMaxValue(for: .audioSpeakerVolume) let volumeOSDValue = self.calcNewValue(currentValue: currentValue, maxValue: maxValue, isUp: isUp, isSmallIncrement: isSmallIncrement) let volumeDDCValue = UInt16(volumeOSDValue) @@ -119,33 +122,24 @@ class ExternalDisplay: Display { } else if !self.isMuted(), volumeOSDValue == 0 { muteValue = 1 } - let isAlreadySet = volumeOSDValue == self.getValue(for: .audioSpeakerVolume) - - guard self.writeDDCValues(command: .audioSpeakerVolume, value: volumeDDCValue) == true else { - return - } - - if let muteValue = muteValue { - if self.enableMuteUnmute { + if !isAlreadySet { + if let muteValue = muteValue, self.enableMuteUnmute { guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else { return } + self.saveValue(muteValue, for: .audioMuteScreenBlank) } - self.saveValue(muteValue, for: .audioMuteScreenBlank) - } + if !self.enableMuteUnmute || volumeOSDValue != 0 { + _ = self.writeDDCValues(command: .audioSpeakerVolume, value: volumeDDCValue) + } + } if !self.hideOsd { self.showOsd(command: .audioSpeakerVolume, value: volumeOSDValue, roundChiclet: !isSmallIncrement) } - if !isAlreadySet { self.saveValue(volumeOSDValue, for: .audioSpeakerVolume) - - if volumeOSDValue > 0 { - self.playVolumeChangedSound() - } - if let slider = self.volumeSliderHandler?.slider { slider.intValue = Int32(volumeDDCValue) } @@ -261,48 +255,43 @@ class ExternalDisplay: Display { self.saveValue(osdValue, for: .brightness) } - public func writeDDCValues(command: DDC.Command, value: UInt16, errorRecoveryWaitTime _: UInt32? = nil) -> Bool? { + public func writeDDCValues(command: Command, value: UInt16, errorRecoveryWaitTime _: UInt32? = nil) -> Bool? { guard app.sleepID == 0, app.reconfigureID == 0, !self.forceSw else { return false } - if Arm64DDCUtils.isArm64 { - guard self.arm64ddc else { - return false + var success: Bool = false + self.ddcQueue.sync { + if Arm64DDC.isArm64 { + if self.arm64ddc { + success = Arm64DDC.write(service: self.arm64avService, command: command.rawValue, value: value) + } + } else { + success = self.ddc?.write(command: command.rawValue, value: value, errorRecoveryWaitTime: 2000) ?? false } - return Arm64DDCUtils.write(service: self.arm64avService, command: command.rawValue, value: value) - } else { - return self.ddc?.write(command: command, value: value, errorRecoveryWaitTime: 2000) ?? false } + return success } - func readDDCValues(for command: DDC.Command, tries: UInt, minReplyDelay delay: UInt64?) -> (current: UInt16, max: UInt16)? { + func readDDCValues(for command: Command, tries: UInt, minReplyDelay delay: UInt64?) -> (current: UInt16, max: UInt16)? { var values: (UInt16, UInt16)? guard app.sleepID == 0, app.reconfigureID == 0, !self.forceSw else { return values } - if Arm64DDCUtils.isArm64 { + if Arm64DDC.isArm64 { guard self.arm64ddc else { return nil } - if let unwrappedDelay = delay { - values = Arm64DDCUtils.read(service: self.arm64avService, command: command.rawValue, tries: UInt8(min(tries, 255)), minReplyDelay: UInt32(unwrappedDelay / 1000)) - } else { - values = Arm64DDCUtils.read(service: self.arm64avService, command: command.rawValue, tries: UInt8(min(tries, 255))) + self.ddcQueue.sync { + if let unwrappedDelay = delay { + values = Arm64DDC.read(service: self.arm64avService, command: command.rawValue, tries: UInt8(min(tries, 255)), minReplyDelay: UInt32(unwrappedDelay / 1000)) + } else { + values = Arm64DDC.read(service: self.arm64avService, command: command.rawValue, tries: UInt8(min(tries, 255))) + } } } else { - if self.ddc?.supported(minReplyDelay: delay) == true { - os_log("Display supports DDC.", type: .debug) - } else { - os_log("Display does not support DDC.", type: .debug) + self.ddcQueue.sync { + values = self.ddc?.read(command: command.rawValue, tries: tries, minReplyDelay: delay) } - - if self.ddc?.enableAppReport() == true { - os_log("Display supports enabling DDC application report.", type: .debug) - } else { - os_log("Display does not support enabling DDC application report.", type: .debug) - } - - values = self.ddc?.read(command: command, tries: tries, minReplyDelay: delay) } return values } @@ -336,28 +325,32 @@ class ExternalDisplay: Display { return max(0, min(maxValue, nextValue)) } - func getValue(for command: DDC.Command) -> Int { + func getValue(for command: Command) -> Int { return self.prefs.integer(forKey: "\(command.rawValue)-\(self.identifier)") } - func saveValue(_ value: Int, for command: DDC.Command) { + func getValueExists(for command: Command) -> Bool { + return self.prefs.object(forKey: "\(command.rawValue)-\(self.identifier)") != nil + } + + func saveValue(_ value: Int, for command: Command) { self.prefs.set(value, forKey: "\(command.rawValue)-\(self.identifier)") } - func saveMaxValue(_ maxValue: Int, for command: DDC.Command) { + func saveMaxValue(_ maxValue: Int, for command: Command) { self.prefs.set(maxValue, forKey: "max-\(command.rawValue)-\(self.identifier)") } - func getMaxValue(for command: DDC.Command) -> Int { + func getMaxValue(for command: Command) -> Int { let max = self.prefs.integer(forKey: "max-\(command.rawValue)-\(self.identifier)") return min(self.DDC_HARD_MAX_LIMIT, max == 0 ? self.DDC_HARD_MAX_LIMIT : max) } - func getRestoreValue(for command: DDC.Command) -> Int { + func getRestoreValue(for command: Command) -> Int { return self.prefs.integer(forKey: "restore-\(command.rawValue)-\(self.identifier)") } - func setRestoreValue(_ value: Int?, for command: DDC.Command) { + func setRestoreValue(_ value: Int?, for command: Command) { self.prefs.set(value, forKey: "restore-\(command.rawValue)-\(self.identifier)") } @@ -382,16 +375,16 @@ class ExternalDisplay: Display { let selectedMode = self.getPollingMode() switch selectedMode { case 0: - return PollingMode.none.value + return Utils.PollingMode.none.value case 1: - return PollingMode.minimal.value + return Utils.PollingMode.minimal.value case 2: - return PollingMode.normal.value + return Utils.PollingMode.normal.value case 3: - return PollingMode.heavy.value + return Utils.PollingMode.heavy.value case 4: let val = self.prefs.integer(forKey: "pollingCount-\(self.identifier)") - return PollingMode.custom(value: val).value + return Utils.PollingMode.custom(value: val).value default: return 0 } @@ -401,31 +394,25 @@ class ExternalDisplay: Display { self.prefs.set(value, forKey: "pollingCount-\(self.identifier)") } - private func stepSize(for command: DDC.Command, isSmallIncrement: Bool) -> Int { + private func stepSize(for command: Command, isSmallIncrement: Bool) -> Int { return isSmallIncrement ? 1 : Int(floor(Float(self.getMaxValue(for: command)) / OSDUtils.chicletCount)) } - override func showOsd(command: DDC.Command, value: Int, maxValue _: Int = 100, roundChiclet: Bool = false, lock: Bool = false) { + override func showOsd(command: Command, value: Int, maxValue _: Int = 100, roundChiclet: Bool = false, lock: Bool = false) { super.showOsd(command: command, value: value, maxValue: self.getMaxValue(for: command), roundChiclet: roundChiclet, lock: lock) } - private func playVolumeChangedSound() { - let soundPath = "/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/volume.aiff" - let soundUrl = URL(fileURLWithPath: soundPath) + private var audioPlayer: AVAudioPlayer? + private func playVolumeChangedSound() { // Check if user has enabled "Play feedback when volume is changed" in Sound Preferences - guard let preferences = Utils.getSystemPreferences(), - let hasSoundEnabled = preferences["com.apple.sound.beep.feedback"] as? Int, - hasSoundEnabled == 1 + guard let preferences = Utils.getSystemPreferences(), let hasSoundEnabled = preferences["com.apple.sound.beep.feedback"] as? Int, hasSoundEnabled == 1 else { - os_log("sound not enabled", type: .info) return } - do { - self.audioPlayer = try AVAudioPlayer(contentsOf: soundUrl) + self.audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: "/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/volume.aiff")) self.audioPlayer?.volume = 1 - self.audioPlayer?.prepareToPlay() self.audioPlayer?.play() } catch { os_log("%{public}@", type: .error, error.localizedDescription) diff --git a/MonitorControl/Support/Arm64DDCUtils.swift b/MonitorControl/Support/Arm64DDC.swift similarity index 97% rename from MonitorControl/Support/Arm64DDCUtils.swift rename to MonitorControl/Support/Arm64DDC.swift index 6945242..c86e5f0 100644 --- a/MonitorControl/Support/Arm64DDCUtils.swift +++ b/MonitorControl/Support/Arm64DDC.swift @@ -1,5 +1,5 @@ // -// Arm64DDCUitls.swift +// Arm64DDC.swift // MonitorControl // // Created by @waydabber, 2021 @@ -9,7 +9,7 @@ import Foundation import IOKit -class Arm64DDCUtils: NSObject { +class Arm64DDC: NSObject { public struct DisplayService { var displayID: CGDirectDisplayID = 0 var service: IOAVService? @@ -60,7 +60,7 @@ class Arm64DDCUtils: NSObject { var values: (UInt16, UInt16)? var send: [UInt8] = [command] var reply = [UInt8](repeating: 0, count: 11) - if Arm64DDCUtils.performDDCCommunication(service: service, send: &send, reply: &reply, readSleepTime: minReplyDelay, numOfRetryAttemps: tries) { + if Arm64DDC.performDDCCommunication(service: service, send: &send, reply: &reply, readSleepTime: minReplyDelay, numOfRetryAttemps: tries) { let max = UInt16(reply[6]) * 256 + UInt16(reply[7]) let current = UInt16(reply[8]) * 256 + UInt16(reply[9]) values = (current, max) @@ -74,7 +74,7 @@ class Arm64DDCUtils: NSObject { public static func write(service: IOAVService?, command: UInt8, value: UInt16) -> Bool { var send: [UInt8] = [command, UInt8(value >> 8), UInt8(value & 255)] var reply: [UInt8] = [] - return Arm64DDCUtils.performDDCCommunication(service: service, send: &send, reply: &reply) + return Arm64DDC.performDDCCommunication(service: service, send: &send, reply: &reply) } // Performs DDC read or write diff --git a/MonitorControl/Support/Bridging-Header.h b/MonitorControl/Support/Bridging-Header.h index 2b70405..1edd23e 100644 --- a/MonitorControl/Support/Bridging-Header.h +++ b/MonitorControl/Support/Bridging-Header.h @@ -15,6 +15,8 @@ extern void DisplayServicesBrightnessChanged(CGDirectDisplayID display, double b extern int DisplayServicesGetBrightness(CGDirectDisplayID display, float *brightness); extern int DisplayServicesSetBrightness(CGDirectDisplayID display, float brightness); +extern void CGSServiceForDisplayNumber(CGDirectDisplayID display, io_service_t* service); + @class NSString; @protocol OSDUIHelperProtocol diff --git a/MonitorControl/Support/Command.swift b/MonitorControl/Support/Command.swift new file mode 100644 index 0000000..f15281f --- /dev/null +++ b/MonitorControl/Support/Command.swift @@ -0,0 +1,168 @@ +public enum Command: UInt8 { + // Display Control + case horizontalFrequency = 0xAC + case verticalFrequency = 0xAE + case sourceColorCoding = 0xB5 + case displayUsageTime = 0xC0 + case displayControllerId = 0xC8 + case displayFirmwareLevel = 0xC9 + case osdLanguage = 0xCC + case powerMode = 0xD6 + case imageMode = 0xDB + case vcpVersion = 0xDF + + // Geometry + case horizontalPosition = 0x20 + case horizontalSize = 0x22 + case horizontalPincushion = 0x24 + case horizontalPincushionBalance = 0x26 + case horizontalConvergenceRB = 0x28 + case horizontalConvergenceMG = 0x29 + case horizontalLinearity = 0x2A + case horizontalLinearityBalance = 0x2C + case verticalPosition = 0x30 + case verticalSize = 0x32 + case verticalPincushion = 0x34 + case verticalPincushionBalance = 0x36 + case verticalConvergenceRB = 0x38 + case verticalConvergenceMG = 0x39 + case verticalLinearity = 0x3A + case verticalLinearityBalance = 0x3C + case horizontalParallelogram = 0x40 + case verticalParallelogram = 0x41 + case horizontalKeystone = 0x42 + case verticalKeystone = 0x43 + case rotation = 0x44 + case topCornerFlare = 0x46 + case topCornerHook = 0x48 + case bottomCornerFlare = 0x4A + case bottomCornerHook = 0x4C + case horizontalMirror = 0x82 + case verticalMirror = 0x84 + case displayScaling = 0x86 + case windowPositionTopLeftX = 0x95 + case windowPositionTopLeftY = 0x96 + case windowPositionBottomRightX = 0x97 + case windowPositionBottomRightY = 0x98 + case scanMode = 0xDA + + // Miscellaneous + case degauss = 0x01 + case newControlValue = 0x02 + case softControls = 0x03 + case activeControl = 0x52 + case performancePreservation = 0x54 + case inputSelect = 0x60 + case ambientLightSensor = 0x66 + case remoteProcedureCall = 0x76 + case displayIdentificationOnDataOperation = 0x78 + case tvChannelUpDown = 0x8B + case flatPanelSubPixelLayout = 0xB2 + case displayTechnologyType = 0xB6 + case displayDescriptorLength = 0xC2 + case transmitDisplayDescriptor = 0xC3 + case enableDisplayOfDisplayDescriptor = 0xC4 + case applicationEnableKey = 0xC6 + case displayEnableKey = 0xC7 + case statusIndicator = 0xCD + case auxiliaryDisplaySize = 0xCE + case auxiliaryDisplayData = 0xCF + case outputSelect = 0xD0 + case assetTag = 0xD2 + case auxiliaryPowerOutput = 0xD7 + case scratchPad = 0xDE + + // Audio + case audioSpeakerVolume = 0x62 + case speakerSelect = 0x63 + case audioMicrophoneVolume = 0x64 + case audioJackConnectionStatus = 0x65 + case audioMuteScreenBlank = 0x8D + case audioTreble = 0x8F + case audioBass = 0x91 + case audioBalanceLR = 0x93 + case audioProcessorMode = 0x94 + + // OSD/Button Event Control + case osd = 0xCA + + // Image Adjustment + case sixAxisHueControlBlue = 0x9F + case sixAxisHueControlCyan = 0x9E + case sixAxisHueControlGreen = 0x9D + case sixAxisHueControlMagenta = 0xA0 + case sixAxisHueControlRed = 0x9B + case sixAxisHueControlYellow = 0x9C + case sixAxisSaturationControlBlue = 0x5D + case sixAxisSaturationControlCyan = 0x5C + case sixAxisSaturationControlGreen = 0x5B + case sixAxisSaturationControlMagenta = 0x5E + case sixAxisSaturationControlRed = 0x59 + case sixAxisSaturationControlYellow = 0x5A + case adjustZoom = 0x7C + case autoColorSetup = 0x1F + case autoSetup = 0x1E + case autoSetupOnOff = 0xA2 + case backlightControlLegacy = 0x13 + case backlightLevelWhite = 0x6B + case backlightLevelRed = 0x6D + case backlightLevelGreen = 0x6F + case backlightLevelBlue = 0x71 + case blockLutOperation = 0x75 + case clock = 0x0E + case clockPhase = 0x3E + case colorSaturation = 0x8A + case colorTemperatureIncrement = 0x0B + case colorTemperatureRequest = 0x0C + case contrast = 0x12 + case displayApplication = 0xDC + case fleshToneEnhancement = 0x11 + case focus = 0x1C + case gamma = 0x72 + case grayScaleExpansion = 0x2E + case horizontalMoire = 0x56 + case hue = 0x90 + case luminance = 0x10 + case lutSize = 0x73 + case screenOrientation = 0xAA + case selectColorPreset = 0x14 + case sharpness = 0x87 + case singlePointLutOperation = 0x74 + case stereoVideoMode = 0xD4 + case tvBlackLevel = 0x92 + case tvContrast = 0x8E + case tvSharpness = 0x8C + case userColorVisionCompensation = 0x17 + case velocityScanModulation = 0x88 + case verticalMoire = 0x58 + case videoBlackLevelBlue = 0x70 + case videoBlackLevelGreen = 0x6E + case videoBlackLevelRed = 0x6C + case videoGainBlue = 0x1A + case videoGainGreen = 0x18 + case videoGainRed = 0x16 + case windowBackground = 0x9A + case windowControlOnOff = 0xA4 + case windowSelect = 0xA5 + case windowSize = 0xA6 + case windowTransparency = 0xA7 + + // Preset Operations + case restoreFactoryDefaults = 0x04 + case restoreFactoryLuminanceContrastDefaults = 0x05 + case restoreFactoryGeometryDefaults = 0x06 + case restoreFactoryColorDefaults = 0x08 + case restoreFactoryTvDefaults = 0x0A + case settings = 0xB0 + + // Manufacturer Specific + case blackStabilizer = 0xF9 // LG 38UC99-W + case colorPresetC = 0xE0 + case powerControl = 0xE1 + case topLeftScreenPurity = 0xE8 + case topRightScreenPurity = 0xE9 + case bottomLeftScreenPurity = 0xEA + case bottomRightScreenPurity = 0xEB + + public static let brightness = luminance +} diff --git a/MonitorControl/Support/IntelDDC.swift b/MonitorControl/Support/IntelDDC.swift new file mode 100644 index 0000000..27e78d5 --- /dev/null +++ b/MonitorControl/Support/IntelDDC.swift @@ -0,0 +1,259 @@ +// +// IntelDDC.swift +// MonitorControl +// +// Original code: https://github.com/reitermarkus/DDC.swift +// Adapted for MonitorControl +// Credits to @reitermarkus +// + +import Foundation +import IOKit.i2c +import os.log + +public class IntelDDC { + let displayId: CGDirectDisplayID + let framebuffer: io_service_t + let replyTransactionType: IOOptionBits + var enabled: Bool = false + + deinit { + assert(IOObjectRelease(self.framebuffer) == KERN_SUCCESS) + } + + public init?(for displayId: CGDirectDisplayID, withReplyTransactionType replyTransactionType: IOOptionBits? = nil) { + self.displayId = displayId + guard let framebuffer = IntelDDC.ioFramebufferPortFromDisplayId(displayId: displayId) else { + return nil + } + self.framebuffer = framebuffer + if let replyTransactionType = replyTransactionType { + self.replyTransactionType = replyTransactionType + } else if let replyTransactionType = IntelDDC.supportedTransactionType() { + self.replyTransactionType = replyTransactionType + } else { + os_log("No supported reply transaction type found for display with ID %u.", type: .error, displayId) + return nil + } + } + + public func write(command: UInt8, value: UInt16, errorRecoveryWaitTime: UInt32? = nil, writeSleepTime: UInt32 = 10000, numofWriteCycles: UInt8 = 2) -> Bool { + let message: [UInt8] = [0x03, command, UInt8(value >> 8), UInt8(value & 0xFF)] + var replyData: [UInt8] = [] + var success: Bool = false + for _ in 1 ... numofWriteCycles { + usleep(writeSleepTime) + if self.sendMessage(message, replyData: &replyData, errorRecoveryWaitTime: errorRecoveryWaitTime ?? 20000) != nil { + success = true + } + } + return success + } + + public func sendMessage(_ message: [UInt8], replyData: inout [UInt8], minReplyDelay: UInt64? = nil, errorRecoveryWaitTime: UInt32? = nil) -> IOI2CRequest? { + var data: [UInt8] = [UInt8(0x51), UInt8(0x80 + message.count)] + message + [UInt8(0x6E)] + for i in 0 ..< (data.count - 1) { + data[data.count - 1] ^= data[i] + } + var request = IOI2CRequest() + request.commFlags = 0 + request.sendAddress = 0x6E + request.sendTransactionType = IOOptionBits(kIOI2CSimpleTransactionType) + request.sendBytes = UInt32(data.count) + request.sendBuffer = withUnsafePointer(to: &data[0]) { vm_address_t(bitPattern: $0) } + if replyData.count == 0 { + request.replyTransactionType = IOOptionBits(kIOI2CNoTransactionType) + request.replyBytes = 0 + } else { + request.minReplyDelay = minReplyDelay ?? 10 + request.replyAddress = 0x6F + request.replySubAddress = 0x51 + request.replyTransactionType = self.replyTransactionType + request.replyBytes = UInt32(replyData.count) + request.replyBuffer = withUnsafePointer(to: &replyData[0]) { vm_address_t(bitPattern: $0) } + } + guard IntelDDC.send(request: &request, to: self.framebuffer, errorRecoveryWaitTime: errorRecoveryWaitTime) else { + return nil + } + if replyData.count > 0 { + let checksum = replyData.last! + var calculated = UInt8(0x50) + for i in 0 ..< (replyData.count - 1) { + calculated ^= replyData[i] + } + guard checksum == calculated else { + os_log("Checksum of reply does not match. Expected %u, got %u.", type: .error, checksum, calculated) + os_log("Response was: %{public}@", type: .debug, replyData.map { String(format: "%02X", $0) }.joined(separator: " ")) + return nil + } + } + return request + } + + public func read(command: UInt8, tries: UInt = 1, replyTransactionType _: IOOptionBits? = nil, minReplyDelay: UInt64? = nil, errorRecoveryWaitTime: UInt32? = nil) -> (UInt16, UInt16)? { + assert(tries > 0) + let message: [UInt8] = [0x01, command] + var replyData: [UInt8] = Array(repeating: 0, count: 11) + for i in 1 ... tries { + guard self.sendMessage(message, replyData: &replyData, minReplyDelay: minReplyDelay, errorRecoveryWaitTime: errorRecoveryWaitTime ?? 40000) != nil else { + continue + } + guard replyData[2] == 0x02 else { + os_log("Got wrong response type for %{public}@. Expected %u, got %u.", type: .debug, String(reflecting: command), 0x02, replyData[2]) + os_log("Response was: %{public}@", type: .debug, replyData.map { String(format: "%02X", $0) }.joined(separator: " ")) + continue + } + guard replyData[3] == 0x00 else { + os_log("Reading %{public}@ is not supported.", type: .debug, String(reflecting: command)) + return nil + } + if i > 1 { + os_log("Reading %{public}@ took %u tries.", type: .debug, String(reflecting: command), i) + } + let (mh, ml, sh, sl) = (replyData[6], replyData[7], replyData[8], replyData[9]) + let maxValue = UInt16(mh << 8) + UInt16(ml) + let currentValue = UInt16(sh << 8) + UInt16(sl) + return (currentValue, maxValue) + } + os_log("Reading %{public}@ failed.", type: .error, String(reflecting: command)) + return nil + } + + private static func supportedTransactionType() -> IOOptionBits? { + var ioIterator = io_iterator_t() + guard IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceNameMatching("IOFramebufferI2CInterface"), &ioIterator) == KERN_SUCCESS else { + return nil + } + defer { + assert(IOObjectRelease(ioIterator) == KERN_SUCCESS) + } + while case let ioService = IOIteratorNext(ioIterator), ioService != 0 { + var serviceProperties: Unmanaged? + guard IORegistryEntryCreateCFProperties(ioService, &serviceProperties, kCFAllocatorDefault, IOOptionBits()) == KERN_SUCCESS, serviceProperties != nil else { + continue + } + let dict = serviceProperties!.takeRetainedValue() as NSDictionary + if let types = dict[kIOI2CTransactionTypesKey] as? UInt64 { + if (1 << kIOI2CDDCciReplyTransactionType) & types != 0 { + os_log("kIOI2CDDCciReplyTransactionType is supported.", type: .debug) + return IOOptionBits(kIOI2CDDCciReplyTransactionType) + } + if (1 << kIOI2CSimpleTransactionType) & types != 0 { + os_log("kIOI2CSimpleTransactionType is supported.", type: .debug) + return IOOptionBits(kIOI2CSimpleTransactionType) + } + } + } + return nil + } + + static func send(request: inout IOI2CRequest, to framebuffer: io_service_t, errorRecoveryWaitTime: UInt32? = nil) -> Bool { + if let errorRecoveryWaitTime = errorRecoveryWaitTime { + usleep(errorRecoveryWaitTime) + } + var busCount: IOItemCount = 0 + guard IOFBGetI2CInterfaceCount(framebuffer, &busCount) == KERN_SUCCESS else { + os_log("Failed to get interface count for framebuffer with ID %u.", type: .error, framebuffer) + return false + } + for bus: IOOptionBits in 0 ..< busCount { + var interface = io_service_t() + guard IOFBCopyI2CInterfaceForBus(framebuffer, bus, &interface) == KERN_SUCCESS else { + os_log("Failed to get interface %u for framebuffer with ID %u.", type: .error, bus, framebuffer) + continue + } + var connect: IOI2CConnectRef? + guard IOI2CInterfaceOpen(interface, IOOptionBits(), &connect) == KERN_SUCCESS else { + os_log("Failed to connect to interface %u for framebuffer with ID %u.", type: .error, bus, framebuffer) + continue + } + defer { IOI2CInterfaceClose(connect, IOOptionBits()) } + guard IOI2CSendRequest(connect, IOOptionBits(), &request) == KERN_SUCCESS else { + os_log("Failed to send request to interface %u for framebuffer with ID %u.", type: .error, bus, framebuffer) + continue + } + guard request.result == KERN_SUCCESS else { + os_log("Request to interface %u for framebuffer with ID %u failed.", type: .error, bus, framebuffer) + continue + } + return true + } + return false + } + + static func servicePortUsingDisplayPropertiesMatching(from displayId: CGDirectDisplayID) -> io_object_t? { + var portIterator = io_iterator_t() + let status: kern_return_t = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(IOFRAMEBUFFER_CONFORMSTO), &portIterator) + guard status == KERN_SUCCESS else { + os_log("No matching services found for display with ID %u.", type: .error, displayId) + return nil + } + defer { + assert(IOObjectRelease(portIterator) == KERN_SUCCESS) + } + while case let port = IOIteratorNext(portIterator), port != 0 { + let dict = IODisplayCreateInfoDictionary(port, IOOptionBits(kIODisplayOnlyPreferredName)).takeRetainedValue() as NSDictionary + let valueForKey = { (k: String) in + (dict[k] as? CFIndex).flatMap { Int32(exactly: $0) }.flatMap { UInt32(bitPattern: $0) } ?? 0 + } + let portVendorId = valueForKey(kDisplayVendorID) + let displayVendorId = CGDisplayVendorNumber(displayId) + guard portVendorId == displayVendorId else { + os_log("Service port vendor ID %u differs from display product ID %u.", type: .debug, + portVendorId, displayVendorId) + continue + } + let portProductId = valueForKey(kDisplayProductID) + let displayProductId = CGDisplayModelNumber(displayId) + guard portProductId == displayProductId else { + os_log("Service port product ID %u differs from display product ID %u.", type: .debug, + portProductId, displayProductId) + continue + } + let portSerialNumber = valueForKey(kDisplaySerialNumber) + let displaySerialNumber = CGDisplaySerialNumber(displayId) + guard portSerialNumber == displaySerialNumber else { + os_log("Service port serial number %u differs from display serial number %u.", type: .debug, portSerialNumber, displaySerialNumber) + continue + } + if let displayLocation = dict[kIODisplayLocationKey] as? NSString { + // the unit number is the number right after the last "@" sign in the display location + // swiftlint:disable:next force_try + let regex = try! NSRegularExpression(pattern: "@([0-9]+)[^@]+$", options: []) + if let match = regex.firstMatch(in: displayLocation as String, options: [], range: NSRange(location: 0, length: displayLocation.length)) { + let unitNumber = UInt32(displayLocation.substring(with: match.range(at: 1))) + guard unitNumber == CGDisplayUnitNumber(displayId) else { + continue + } + } + } + os_log("Vendor ID: %u, Product ID: %u, Serial Number: %u", type: .debug, portVendorId, portProductId, portSerialNumber) + os_log("Unit Number: %u", type: .debug, CGDisplayUnitNumber(displayId)) + os_log("Service Port: %u", type: .debug, port) + return port + } + os_log("No service port found for display with ID %u.", type: .error, displayId) + return nil + } + + static func ioFramebufferPortFromDisplayId(displayId: CGDirectDisplayID) -> io_service_t? { + if CGDisplayIsBuiltin(displayId) == boolean_t(truncating: true) { + return nil + } + var servicePortUsingCGSServiceForDisplayNumber: io_service_t = 0 + CGSServiceForDisplayNumber(displayId, &servicePortUsingCGSServiceForDisplayNumber) + if servicePortUsingCGSServiceForDisplayNumber != 0 { + os_log("Using CGSServiceForDisplayNumber to acquire framebuffer port for %u.", type: .debug, displayId) + return servicePortUsingCGSServiceForDisplayNumber + } + guard let servicePort = self.servicePortUsingDisplayPropertiesMatching(from: displayId) else { + return nil + } + var busCount: IOItemCount = 0 + guard IOFBGetI2CInterfaceCount(servicePort, &busCount) == KERN_SUCCESS, busCount >= 1 else { + os_log("No framebuffer port found for display with ID %u.", type: .error, displayId) + return nil + } + return servicePort + } +} diff --git a/MonitorControl/Support/OnlyIntegerValueFormatter.swift b/MonitorControl/Support/OnlyIntegerValueFormatter.swift deleted file mode 100644 index 0b2acf8..0000000 --- a/MonitorControl/Support/OnlyIntegerValueFormatter.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa - -class OnlyIntegerValueFormatter: NumberFormatter { - override func isPartialStringValid(_ partialString: String, newEditingString _: AutoreleasingUnsafeMutablePointer?, errorDescription _: AutoreleasingUnsafeMutablePointer?) -> Bool { - if partialString.isEmpty { - return true - } - - if partialString.count > 3 { - return false - } - - return Int(partialString) != nil - } -} diff --git a/MonitorControl/Support/PollingMode.swift b/MonitorControl/Support/PollingMode.swift deleted file mode 100644 index 30d2716..0000000 --- a/MonitorControl/Support/PollingMode.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -enum PollingMode { - case none - case minimal - case normal - case heavy - case custom(value: Int) - - var value: Int { - switch self { - case .none: - return 0 - case .minimal: - return 5 - case .normal: - return 10 - case .heavy: - return 100 - case let .custom(val): - return val - } - } -} diff --git a/MonitorControl/Support/Utils.swift b/MonitorControl/Support/Utils.swift index d6987fb..aec9c56 100644 --- a/MonitorControl/Support/Utils.swift +++ b/MonitorControl/Support/Utils.swift @@ -1,134 +1,8 @@ import Cocoa -import DDC import os.log import ServiceManagement class Utils: NSObject { - // MARK: - Menu - - /// Create a slider and add it to the menu - /// - /// - Parameters: - /// - menu: Menu containing the slider - /// - display: Display to control - /// - command: Command (Brightness/Volume/...) - /// - title: Title of the slider - /// - Returns: An `NSSlider` slider - static func addSliderMenuItem(toMenu menu: NSMenu, forDisplay display: ExternalDisplay, command: DDC.Command, title: String, numOfTickMarks: Int = 0) -> SliderHandler { - let item = NSMenuItem() - - let handler = SliderHandler(display: display, command: command) - - let slider = NSSlider(value: 0, minValue: 0, maxValue: 100, target: handler, action: #selector(SliderHandler.valueChanged)) - slider.isEnabled = false - handler.slider = slider - - if #available(macOS 11.0, *) { - slider.frame.size.width = 160 - slider.frame.origin = NSPoint(x: 35, y: 5) - let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 47, height: slider.frame.height + 14)) - view.frame.origin = NSPoint(x: 12, y: 0) - var iconName: String = "circle.dashed" - switch command { - case .audioSpeakerVolume: iconName = "speaker.wave.2" - case .brightness: iconName = "sun.max" - case .contrast: iconName = "circle.lefthalf.fill" - default: break - } - let icon = NSImageView(image: NSImage(systemSymbolName: iconName, accessibilityDescription: title)!) - icon.frame = view.frame - icon.wantsLayer = true - icon.alphaValue = 0.7 - icon.imageAlignment = NSImageAlignment.alignLeft - view.addSubview(icon) - view.addSubview(slider) - item.view = view - menu.insertItem(item, at: 0) - } else { - slider.frame.size.width = 180 - slider.frame.origin = NSPoint(x: 15, y: 5) - let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30, height: slider.frame.height + 10)) - let sliderHeaderItem = NSMenuItem() - let attrs: [NSAttributedString.Key: Any] = [.foregroundColor: NSColor.systemGray, .font: NSFont.systemFont(ofSize: 12)] - sliderHeaderItem.attributedTitle = NSAttributedString(string: title, attributes: attrs) - view.addSubview(slider) - item.view = view - menu.insertItem(item, at: 0) - menu.insertItem(sliderHeaderItem, at: 0) - } - - var values: (UInt16, UInt16)? - let delay = display.needsLongerDelay ? UInt64(40 * kMillisecondScale) : nil - - let tries = UInt(display.getPollingCount()) - os_log("Polling %{public}@ times", type: .info, String(tries)) - - var (currentValue, maxValue) = (UInt16(0), UInt16(0)) - - if display.isSw(), command == DDC.Command.brightness { - (currentValue, maxValue) = (UInt16(display.getSwBrightnessPrefValue()), UInt16(display.getSwMaxBrightness())) - } else { - if tries != 0 { - values = display.readDDCValues(for: command, tries: tries, minReplyDelay: delay) - } - (currentValue, maxValue) = values ?? (UInt16(display.getValue(for: command)), 0) // We set 0 for max. value to indicate that there is no real DDC reported max. value - ExternalDisplay.getMaxValue() will return 100 in case of 0 max. values. - } - display.saveMaxValue(Int(maxValue), for: command) - display.saveValue(min(Int(currentValue), display.getMaxValue(for: command)), for: command) // We won't allow currrent value to be higher than the max. value - os_log("%{public}@ (%{public}@):", type: .info, display.name, String(reflecting: command)) - os_log(" - current value: %{public}@ - from display? %{public}@", type: .info, String(currentValue), String(values != nil)) - os_log(" - maximum value: %{public}@ - from display? %{public}@", type: .info, String(display.getMaxValue(for: command)), String(values != nil)) - - if command == .brightness { - if !display.isSw(), prefs.bool(forKey: Utils.PrefKeys.lowerSwAfterBrightness.rawValue) { - slider.maxValue = Double(display.getMaxValue(for: command) * 2) - slider.integerValue = Int(slider.maxValue) / 2 + Int(currentValue) - } else { - slider.integerValue = Int(currentValue) - slider.maxValue = Double(display.getMaxValue(for: command)) - } - } else if command == .audioSpeakerVolume { - // If we're looking at the audio speaker volume, also retrieve the values for the mute command - var muteValues: (current: UInt16, max: UInt16)? - - os_log("Polling %{public}@ times", type: .info, String(tries)) - os_log("%{public}@ (%{public}@):", type: .info, display.name, String(reflecting: DDC.Command.audioMuteScreenBlank)) - - if tries != 0 { - muteValues = display.readDDCValues(for: .audioMuteScreenBlank, tries: tries, minReplyDelay: delay) - } - - if let muteValues = muteValues { - os_log(" - current ddc value: %{public}@", type: .info, String(muteValues.current)) - os_log(" - maximum ddc value: %{public}@", type: .info, String(muteValues.max)) - - display.saveValue(Int(muteValues.current), for: .audioMuteScreenBlank) - display.saveMaxValue(Int(muteValues.max), for: .audioMuteScreenBlank) - } else { - os_log(" - current ddc value: unknown", type: .info) - os_log(" - stored maximum ddc value: %{public}@", type: .info, String(display.getMaxValue(for: .audioMuteScreenBlank))) - } - - // If the system is not currently muted, or doesn't support the mute command, display the current volume as the slider value - if muteValues == nil || muteValues!.current == 2 { - slider.integerValue = Int(currentValue) - } else { - slider.integerValue = 0 - } - - slider.maxValue = Double(display.getMaxValue(for: command)) - } else { - slider.integerValue = Int(currentValue) - slider.maxValue = Double(display.getMaxValue(for: command)) - } - - slider.numberOfTickMarks = numOfTickMarks - slider.isEnabled = true - return handler - } - - // MARK: - Utilities - /// Acquire Privileges (Necessary to listen to keyboard event globally) static func acquirePrivileges() { if !self.readPrivileges(prompt: true) { @@ -176,9 +50,12 @@ class Utils: NSObject { return chkd } - static func alert(text: String) { + static func alert(text: String, info: String = "") { let alert = NSAlert() alert.messageText = text + if info != "" { + alert.informativeText = info + } alert.alertStyle = NSAlert.Style.informational alert.addButton(withTitle: "OK") alert.runModal() @@ -242,4 +119,27 @@ class Utils: NSObject { /// Don't listen for any keys case none = 3 } + + enum PollingMode { + case none + case minimal + case normal + case heavy + case custom(value: Int) + + var value: Int { + switch self { + case .none: + return 0 + case .minimal: + return 5 + case .normal: + return 10 + case .heavy: + return 100 + case let .custom(val): + return val + } + } + } } diff --git a/MonitorControl/Support/de.lproj/Localizable.strings b/MonitorControl/Support/de.lproj/Localizable.strings index 2d6ac9b..eed1839 100644 --- a/MonitorControl/Support/de.lproj/Localizable.strings +++ b/MonitorControl/Support/de.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Einstellungen zurücksetzen?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Kurzbefehle nicht verfügbar"; diff --git a/MonitorControl/Support/en.lproj/Localizable.strings b/MonitorControl/Support/en.lproj/Localizable.strings index ccbb2d8..8d6dd2a 100644 --- a/MonitorControl/Support/en.lproj/Localizable.strings +++ b/MonitorControl/Support/en.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Preferences?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Shortcuts not available"; diff --git a/MonitorControl/Support/es-419.lproj/Localizable.strings b/MonitorControl/Support/es-419.lproj/Localizable.strings index f7b4191..5960c94 100644 --- a/MonitorControl/Support/es-419.lproj/Localizable.strings +++ b/MonitorControl/Support/es-419.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "¿Restablecer las preferencias?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Atajos no disponibles"; diff --git a/MonitorControl/Support/fr.lproj/Localizable.strings b/MonitorControl/Support/fr.lproj/Localizable.strings index 3e66af2..197e47b 100644 --- a/MonitorControl/Support/fr.lproj/Localizable.strings +++ b/MonitorControl/Support/fr.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Preferences?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Raccourcis non disponible"; diff --git a/MonitorControl/Support/hu.lproj/Localizable.strings b/MonitorControl/Support/hu.lproj/Localizable.strings index 5b758d1..c6b1bfe 100644 --- a/MonitorControl/Support/hu.lproj/Localizable.strings +++ b/MonitorControl/Support/hu.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Alapértelmezett beállítások"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Biztonsági mód engedélyezve"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "A Shift gomb le lett nyomva indítás közben, az alkalmazás biztonsági módban indult el. Az alapértelmezett beállításokat visszaállítottuk, a DDC olvasás letiltásra került."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Gyorsbillentyűk nem elérhetők"; diff --git a/MonitorControl/Support/it.lproj/Localizable.strings b/MonitorControl/Support/it.lproj/Localizable.strings index b4902f2..4d3e7ab 100644 --- a/MonitorControl/Support/it.lproj/Localizable.strings +++ b/MonitorControl/Support/it.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Preferences?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Comandi rapidi non disponibili"; diff --git a/MonitorControl/Support/ja.lproj/Localizable.strings b/MonitorControl/Support/ja.lproj/Localizable.strings index 10acd33..eddf388 100644 --- a/MonitorControl/Support/ja.lproj/Localizable.strings +++ b/MonitorControl/Support/ja.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Preferences?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "ショートカットキーを使用できません"; diff --git a/MonitorControl/Support/nl.lproj/Localizable.strings b/MonitorControl/Support/nl.lproj/Localizable.strings index 1c29de0..0d99af2 100644 --- a/MonitorControl/Support/nl.lproj/Localizable.strings +++ b/MonitorControl/Support/nl.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Voorkeuren?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Shortcuts niet beschikbaar"; diff --git a/MonitorControl/Support/pl.lproj/Localizable.strings b/MonitorControl/Support/pl.lproj/Localizable.strings index 46d29f4..166b159 100644 --- a/MonitorControl/Support/pl.lproj/Localizable.strings +++ b/MonitorControl/Support/pl.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Zresetować Preferencje?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Skróty klawiszowe niedostępne"; diff --git a/MonitorControl/Support/ru.lproj/Localizable.strings b/MonitorControl/Support/ru.lproj/Localizable.strings index 831d1f0..663ca00 100644 --- a/MonitorControl/Support/ru.lproj/Localizable.strings +++ b/MonitorControl/Support/ru.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Preferences?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Сочетания клавиш недоступны"; diff --git a/MonitorControl/Support/tr.lproj/Localizable.strings b/MonitorControl/Support/tr.lproj/Localizable.strings index c78970c..44c5fa9 100644 --- a/MonitorControl/Support/tr.lproj/Localizable.strings +++ b/MonitorControl/Support/tr.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Ayarları Sıfırla?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Kısayollar bulunamadı"; diff --git a/MonitorControl/Support/uk.lproj/Localizable.strings b/MonitorControl/Support/uk.lproj/Localizable.strings index 52f9ee2..87b9e2d 100644 --- a/MonitorControl/Support/uk.lproj/Localizable.strings +++ b/MonitorControl/Support/uk.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Preferences?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "Скорочення недоступні"; diff --git a/MonitorControl/Support/zh-Hans.lproj/Localizable.strings b/MonitorControl/Support/zh-Hans.lproj/Localizable.strings index 4ff183d..14afa30 100644 --- a/MonitorControl/Support/zh-Hans.lproj/Localizable.strings +++ b/MonitorControl/Support/zh-Hans.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "Reset Preferences?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "快捷键不可用"; diff --git a/MonitorControl/Support/zh-Hant-TW.lproj/Localizable.strings b/MonitorControl/Support/zh-Hant-TW.lproj/Localizable.strings index c46976e..0259efd 100644 --- a/MonitorControl/Support/zh-Hant-TW.lproj/Localizable.strings +++ b/MonitorControl/Support/zh-Hant-TW.lproj/Localizable.strings @@ -64,6 +64,12 @@ /* Shown in the alert dialog */ "Reset Preferences?" = "重置偏好設定?"; +/* Shown in the alert dialog */ +"Safe Mode Activated" = "Safe Mode Activated"; + +/* Shown in the alert dialog */ +"Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked." = "Shift was pressed during launch. MonitorControl started in safe mode. Default preferences are reloaded, DDC read is blocked."; + /* Shown in the alert dialog */ "Shortcuts not available" = "快捷鍵不可使用"; diff --git a/MonitorControl/UI/Base.lproj/Main.storyboard b/MonitorControl/UI/Base.lproj/Main.storyboard index ca65296..e1544f0 100644 --- a/MonitorControl/UI/Base.lproj/Main.storyboard +++ b/MonitorControl/UI/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + diff --git a/MonitorControl/UI/SliderHandler.swift b/MonitorControl/UI/SliderHandler.swift index 91d9681..7828eed 100644 --- a/MonitorControl/UI/SliderHandler.swift +++ b/MonitorControl/UI/SliderHandler.swift @@ -1,12 +1,12 @@ import Cocoa -import DDC +import os.log class SliderHandler { var slider: NSSlider? var display: ExternalDisplay - let cmd: DDC.Command + let cmd: Command - public init(display: ExternalDisplay, command: DDC.Command) { + public init(display: ExternalDisplay, command: Command) { self.display = display self.cmd = command } @@ -15,6 +15,7 @@ class SliderHandler { guard app.sleepID == 0, app.reconfigureID == 0 else { return } + let snapInterval = 25 let snapThreshold = 3 @@ -32,7 +33,7 @@ class SliderHandler { } if !self.display.isSw() { - if self.cmd == DDC.Command.brightness, prefs.bool(forKey: Utils.PrefKeys.lowerSwAfterBrightness.rawValue) { + if self.cmd == Command.brightness, prefs.bool(forKey: Utils.PrefKeys.lowerSwAfterBrightness.rawValue) { var brightnessDDCValue: Int = 0 var brightnessSwValue: Int = 100 if value >= Int(slider.maxValue / 2) { @@ -45,13 +46,129 @@ class SliderHandler { _ = self.display.writeDDCValues(command: self.cmd, value: UInt16(brightnessDDCValue)) _ = self.display.setSwBrightness(value: UInt8(brightnessSwValue)) self.display.saveValue(brightnessDDCValue, for: self.cmd) + } else if self.cmd == Command.audioSpeakerVolume { + if !self.display.enableMuteUnmute || value != 0 { + _ = self.display.writeDDCValues(command: self.cmd, value: UInt16(value)) + } + self.display.saveValue(value, for: self.cmd) } else { _ = self.display.writeDDCValues(command: self.cmd, value: UInt16(value)) self.display.saveValue(value, for: self.cmd) } - } else if self.cmd == DDC.Command.brightness { + } else if self.cmd == Command.brightness { _ = self.display.setSwBrightness(value: UInt8(value)) self.display.saveValue(value, for: self.cmd) } } + + static func addSliderMenuItem(toMenu menu: NSMenu, forDisplay display: ExternalDisplay, command: Command, title: String, numOfTickMarks: Int = 0) -> SliderHandler { + let item = NSMenuItem() + + let handler = SliderHandler(display: display, command: command) + + let slider = NSSlider(value: 0, minValue: 0, maxValue: 100, target: handler, action: #selector(SliderHandler.valueChanged)) + slider.isEnabled = false + handler.slider = slider + + if #available(macOS 11.0, *) { + slider.frame.size.width = 160 + slider.frame.origin = NSPoint(x: 35, y: 5) + let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 47, height: slider.frame.height + 14)) + view.frame.origin = NSPoint(x: 12, y: 0) + var iconName: String = "circle.dashed" + switch command { + case .audioSpeakerVolume: iconName = "speaker.wave.2" + case .brightness: iconName = "sun.max" + case .contrast: iconName = "circle.lefthalf.fill" + default: break + } + let icon = NSImageView(image: NSImage(systemSymbolName: iconName, accessibilityDescription: title)!) + icon.frame = view.frame + icon.wantsLayer = true + icon.alphaValue = 0.7 + icon.imageAlignment = NSImageAlignment.alignLeft + view.addSubview(icon) + view.addSubview(slider) + item.view = view + menu.insertItem(item, at: 0) + } else { + slider.frame.size.width = 180 + slider.frame.origin = NSPoint(x: 15, y: 5) + let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30, height: slider.frame.height + 10)) + let sliderHeaderItem = NSMenuItem() + let attrs: [NSAttributedString.Key: Any] = [.foregroundColor: NSColor.systemGray, .font: NSFont.systemFont(ofSize: 12)] + sliderHeaderItem.attributedTitle = NSAttributedString(string: title, attributes: attrs) + view.addSubview(slider) + item.view = view + menu.insertItem(item, at: 0) + menu.insertItem(sliderHeaderItem, at: 0) + } + + var values: (UInt16, UInt16)? + let delay = display.needsLongerDelay ? UInt64(40 * kMillisecondScale) : nil + + var (currentValue, maxValue) = (UInt16(0), UInt16(0)) + + let tries = UInt(display.getPollingCount()) + + if display.isSw(), command == Command.brightness { + (currentValue, maxValue) = (UInt16(display.getSwBrightnessPrefValue()), UInt16(display.getSwMaxBrightness())) + } else { + if tries != 0, !(app.safeMode) { + os_log("Polling %{public}@ times", type: .info, String(tries)) + values = display.readDDCValues(for: command, tries: tries, minReplyDelay: delay) + } + (currentValue, maxValue) = values ?? (UInt16(display.getValueExists(for: command) ? display.getValue(for: command) : 75), 100) // We set 100 as max value if we could not read DDC, the previous setting as current value or 75 if not present. + } + display.saveMaxValue(Int(maxValue), for: command) + display.saveValue(min(Int(currentValue), display.getMaxValue(for: command)), for: command) // We won't allow currrent value to be higher than the max. value + os_log("%{public}@ (%{public}@):", type: .info, display.name, String(reflecting: command)) + os_log(" - current value: %{public}@ - from display? %{public}@", type: .info, String(currentValue), String(values != nil)) + os_log(" - maximum value: %{public}@ - from display? %{public}@", type: .info, String(display.getMaxValue(for: command)), String(values != nil)) + + if command == .brightness { + if !display.isSw(), prefs.bool(forKey: Utils.PrefKeys.lowerSwAfterBrightness.rawValue) { + slider.maxValue = Double(display.getMaxValue(for: command) * 2) + slider.integerValue = Int(slider.maxValue) / 2 + Int(currentValue) + } else { + slider.integerValue = Int(currentValue) + slider.maxValue = Double(display.getMaxValue(for: command)) + } + } else if command == .audioSpeakerVolume { + // If we're looking at the audio speaker volume, also retrieve the values for the mute command + var muteValues: (current: UInt16, max: UInt16)? + + if display.enableMuteUnmute, tries != 0, !app.safeMode { + os_log("Polling %{public}@ times", type: .info, String(tries)) + os_log("%{public}@ (%{public}@):", type: .info, display.name, String(reflecting: Command.audioMuteScreenBlank)) + muteValues = display.readDDCValues(for: .audioMuteScreenBlank, tries: tries, minReplyDelay: delay) + } + + if let muteValues = muteValues { + os_log(" - current ddc value: %{public}@", type: .info, String(muteValues.current)) + os_log(" - maximum ddc value: %{public}@", type: .info, String(muteValues.max)) + display.saveValue(Int(muteValues.current), for: .audioMuteScreenBlank) + display.saveMaxValue(Int(muteValues.max), for: .audioMuteScreenBlank) + } else { + os_log(" - current ddc value: unknown", type: .info) + os_log(" - stored maximum ddc value: %{public}@", type: .info, String(display.getMaxValue(for: .audioMuteScreenBlank))) + } + + // If the system is not currently muted, or doesn't support the mute command, display the current volume as the slider value + if muteValues == nil || muteValues!.current == 2 { + slider.integerValue = Int(currentValue) + } else { + slider.integerValue = 0 + } + + slider.maxValue = Double(display.getMaxValue(for: command)) + } else { + slider.integerValue = Int(currentValue) + slider.maxValue = Double(display.getMaxValue(for: command)) + } + + slider.numberOfTickMarks = numOfTickMarks + slider.isEnabled = true + return handler + } } diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index 6f37b89..35b591b 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 2886 + 3094 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly