diff --git a/Cartfile b/Cartfile index b19a532..6d10a3f 100644 --- a/Cartfile +++ b/Cartfile @@ -1,4 +1,4 @@ -github "the0neyouseek/MediaKeyTap" +github "the0neyouseek/MediaKeyTap" "master" github "reitermarkus/DDC.swift" "master" github "rnine/AMCoreAudio" github "shpakovski/MASPreferences" diff --git a/Cartfile.resolved b/Cartfile.resolved index 464c2c5..5887f69 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ -github "reitermarkus/DDC.swift" "41e7c49b0450033c5349ca1cf5234a26ebc011b8" +github "reitermarkus/DDC.swift" "1763870c94c555ff93878caaec8235fd3a9a429d" github "rnine/AMCoreAudio" "3.3.1" github "shpakovski/MASPreferences" "1.3" -github "the0neyouseek/MediaKeyTap" "3.1.0" +github "the0neyouseek/MediaKeyTap" "4314a361486c2907956756748939c61f460241bd" diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj index ffaaae8..f3bdcef 100644 --- a/MonitorControl.xcodeproj/project.pbxproj +++ b/MonitorControl.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ 6C85EFDD22CBAA8F00227EA1 /* PollingModeCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85EFDC22CBAA8F00227EA1 /* PollingModeCellView.swift */; }; 6C85EFDF22CBB54100227EA1 /* PollingCountCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85EFDE22CBB54100227EA1 /* PollingCountCellView.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 */; }; + 6CBFE27C23DB27A200D1BC41 /* InternalDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBFE27B23DB27A200D1BC41 /* InternalDisplay.swift */; }; 6CCB278622D5315200619B05 /* HideOsdCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CCB278522D5315200619B05 /* HideOsdCellView.swift */; }; 6CD444C322D4FBB8005BFD3D /* LongerDelayCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD444C222D4FBB8005BFD3D /* LongerDelayCellView.swift */; }; F01B0699228221B7008E64DB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F01B0680228221B6008E64DB /* Localizable.strings */; }; @@ -42,7 +44,7 @@ F01B069F228221B7008E64DB /* SliderHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01B068F228221B7008E64DB /* SliderHandler.swift */; }; F01B06A0228221B7008E64DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F01B0690228221B7008E64DB /* Main.storyboard */; }; F01B06A1228221B7008E64DB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = F01B0692228221B7008E64DB /* MainMenu.xib */; }; - F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* Display.swift */; }; + F03A8DF21FFBAA6F0034DC27 /* ExternalDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* ExternalDisplay.swift */; }; F03FE4C0228DF62B001F59A4 /* FriendlyNameCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03FE4BF228DF62A001F59A4 /* FriendlyNameCellView.swift */; }; F0445D3820023E710025AE82 /* MainPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D3720023E710025AE82 /* MainPrefsViewController.swift */; }; F0445D3D200254FA0025AE82 /* KeysPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D3B200254FA0025AE82 /* KeysPrefsViewController.swift */; }; @@ -125,6 +127,8 @@ 6CAD134E23624CC1009BD53F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Main.strings; sourceTree = ""; }; 6CAD134F23624CC1009BD53F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/MainMenu.strings; sourceTree = ""; }; 6CAD135023624CC1009BD53F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + 6CBFE27923DB266000D1BC41 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = ""; }; + 6CBFE27B23DB27A200D1BC41 /* InternalDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalDisplay.swift; sourceTree = ""; }; 6CCB278522D5315200619B05 /* HideOsdCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideOsdCellView.swift; sourceTree = ""; }; 6CD444C222D4FBB8005BFD3D /* LongerDelayCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongerDelayCellView.swift; sourceTree = ""; }; B0C4810623357CE500053F91 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; @@ -146,7 +150,7 @@ F01B06A522822215008E64DB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; F01B06A622822217008E64DB /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; F01B06A72282221B008E64DB /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = ""; }; - F03A8DF11FFBAA6F0034DC27 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = ""; }; + F03A8DF11FFBAA6F0034DC27 /* ExternalDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalDisplay.swift; sourceTree = ""; }; F03FE4BF228DF62A001F59A4 /* FriendlyNameCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FriendlyNameCellView.swift; sourceTree = ""; }; F0445D3720023E710025AE82 /* MainPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPrefsViewController.swift; sourceTree = ""; }; F0445D3B200254FA0025AE82 /* KeysPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysPrefsViewController.swift; sourceTree = ""; }; @@ -232,11 +236,11 @@ 56754EAD1D9A4016007BCDC5 /* MonitorControl */ = { isa = PBXGroup; children = ( + 6C6C34F423DB25BF00C0E9CB /* Model */, F01B0686228221B6008E64DB /* Info.plist */, 6C85EFD622C74B0E00227EA1 /* Manager */, 56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */, 56754EB01D9A4016007BCDC5 /* Assets.xcassets */, - F03A8DF11FFBAA6F0034DC27 /* Display.swift */, 28D1DDEB227FB8E9004CB494 /* Extensions */, F01B067F228221B6008E64DB /* Support */, F01B0687228221B6008E64DB /* UI */, @@ -245,6 +249,16 @@ path = MonitorControl; sourceTree = ""; }; + 6C6C34F423DB25BF00C0E9CB /* Model */ = { + isa = PBXGroup; + children = ( + F03A8DF11FFBAA6F0034DC27 /* ExternalDisplay.swift */, + 6CBFE27923DB266000D1BC41 /* Display.swift */, + 6CBFE27B23DB27A200D1BC41 /* InternalDisplay.swift */, + ); + path = Model; + sourceTree = ""; + }; 6C85EFD622C74B0E00227EA1 /* Manager */ = { isa = PBXGroup; children = ( @@ -512,7 +526,9 @@ 6CD444C322D4FBB8005BFD3D /* LongerDelayCellView.swift in Sources */, F03FE4C0228DF62B001F59A4 /* FriendlyNameCellView.swift in Sources */, 6C2EA1CF228F7DFB00060E3F /* PollingMode.swift in Sources */, - F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */, + 6CBFE27C23DB27A200D1BC41 /* InternalDisplay.swift in Sources */, + 6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */, + F03A8DF21FFBAA6F0034DC27 /* ExternalDisplay.swift in Sources */, F0445D40200259C10025AE82 /* DisplayPrefsViewController.swift in Sources */, 28D1DDF0227FBD99004CB494 /* EDID+Extension.swift in Sources */, 6C85EFDD22CBAA8F00227EA1 /* PollingModeCellView.swift in Sources */, @@ -703,7 +719,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 570; + CURRENT_PROJECT_VERSION = 631; DEVELOPMENT_TEAM = CYC8C8R4K9; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -730,7 +746,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 570; + CURRENT_PROJECT_VERSION = 631; DEVELOPMENT_TEAM = CYC8C8R4K9; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -761,7 +777,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 570; + CURRENT_PROJECT_VERSION = 631; DEVELOPMENT_TEAM = CYC8C8R4K9; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -786,7 +802,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 570; + CURRENT_PROJECT_VERSION = 631; DEVELOPMENT_TEAM = CYC8C8R4K9; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/MonitorControl/AppDelegate.swift b/MonitorControl/AppDelegate.swift index 68d282f..54aa4a8 100644 --- a/MonitorControl/AppDelegate.swift +++ b/MonitorControl/AppDelegate.swift @@ -77,87 +77,74 @@ class AppDelegate: NSObject, NSApplicationDelegate { } self.monitorItems = [] - self.displayManager?.clearDisplays() + DisplayManager.shared.clearDisplays() } func updateDisplays() { self.clearDisplays() - let filteredScreens = NSScreen.screens.filter { screen -> Bool in - // Skip built-in displays. + for screen in NSScreen.screens { + let name = screen.displayName ?? NSLocalizedString("Unknown", comment: "Unknown display name") + let id = screen.displayID + let vendorNumber = screen.vendorNumber + let modelNumber = screen.modelNumber + let display: Display if screen.isBuiltin { - return false + display = InternalDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber) + } else { + display = ExternalDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber) } - return DDC(for: screen.displayID)?.edid() != nil + DisplayManager.shared.addDisplay(display: display) } - switch filteredScreens.count { - case 0: - // If no DDC capable display was detected + let ddcDisplays = DisplayManager.shared.getDdcCapableDisplays() + if ddcDisplays.count == 0 { let item = NSMenuItem() item.title = NSLocalizedString("No supported display found", comment: "Shown in menu") item.isEnabled = false self.monitorItems.append(item) self.statusMenu.insertItem(item, at: 0) self.statusMenu.insertItem(NSMenuItem.separator(), at: 1) - default: - os_log("The following supported displays were found:", type: .info) - - for screen in filteredScreens { - os_log(" - %{public}@", type: .info, "\(screen.displayName ?? NSLocalizedString("Unknown", comment: "Unknown display name")) (Vendor: \(screen.vendorNumber ?? 0), Model: \(screen.modelNumber ?? 0))") - self.addScreenToMenu(screen: screen, asSubMenu: filteredScreens.count > 1) + } else { + for display in ddcDisplays { + os_log("Supported display found: %{public}@", type: .info, "\(display.name) (Vendor: \(display.vendorNumber ?? 0), Model: \(display.modelNumber ?? 0))") + self.addDisplayToMenu(display: display, asSubMenu: ddcDisplays.count > 1) } } } - /// Add a screen to the menu - /// - /// - Parameters: - /// - screen: The screen to add - /// - asSubMenu: Display in a sub menu or directly in menu - private func addScreenToMenu(screen: NSScreen, asSubMenu: Bool) { - let id = screen.displayID - let ddc = DDC(for: id) + private func addDisplayToMenu(display: ExternalDisplay, asSubMenu: Bool) { + let monitorSubMenu: NSMenu = asSubMenu ? NSMenu() : self.statusMenu - if let edid = ddc?.edid() { - let name = Utils.getDisplayName(forEdid: edid) - let isEnabled = (prefs.object(forKey: "\(id)-state") as? Bool) ?? true + self.statusMenu.insertItem(NSMenuItem.separator(), at: 0) - let display = Display(id, name: name, isBuiltin: screen.isBuiltin, isEnabled: isEnabled) - - let monitorSubMenu: NSMenu = asSubMenu ? NSMenu() : self.statusMenu - - self.statusMenu.insertItem(NSMenuItem.separator(), at: 0) - - let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, - forDisplay: display, - command: .audioSpeakerVolume, - title: NSLocalizedString("Volume", comment: "Shown in menu")) - let brightnessSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, - forDisplay: display, - command: .brightness, - title: NSLocalizedString("Brightness", comment: "Shown in menu")) - 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")) - display.contrastSliderHandler = contrastSliderHandler - } - - display.volumeSliderHandler = volumeSliderHandler - display.brightnessSliderHandler = brightnessSliderHandler - self.displayManager?.addDisplay(display: display) - - let monitorMenuItem = NSMenuItem() - monitorMenuItem.title = "\(display.getFriendlyName())" - if asSubMenu { - monitorMenuItem.submenu = monitorSubMenu - } - - self.monitorItems.append(monitorMenuItem) - self.statusMenu.insertItem(monitorMenuItem, at: 0) + let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, + forDisplay: display, + command: .audioSpeakerVolume, + title: NSLocalizedString("Volume", comment: "Shown in menu")) + let brightnessSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, + forDisplay: display, + command: .brightness, + title: NSLocalizedString("Brightness", comment: "Shown in menu")) + 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")) + display.contrastSliderHandler = contrastSliderHandler } + + display.volumeSliderHandler = volumeSliderHandler + display.brightnessSliderHandler = brightnessSliderHandler + + let monitorMenuItem = NSMenuItem() + monitorMenuItem.title = "\(display.getFriendlyName())" + if asSubMenu { + monitorMenuItem.submenu = monitorSubMenu + } + + self.monitorItems.append(monitorMenuItem) + self.statusMenu.insertItem(monitorMenuItem, at: 0) } private func setupViewControllers() { @@ -173,12 +160,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { advancedPrefsVc, ] prefsController = MASPreferencesWindowController(viewControllers: views, title: NSLocalizedString("Preferences", comment: "Shown in Preferences window")) - if let displayPrefs = displayPrefsVc as? DisplayPrefsViewController { - displayPrefs.displayManager = self.displayManager - } - if let advancedPrefs = advancedPrefsVc as? AdvancedPrefsViewController { - advancedPrefs.displayManager = self.displayManager - } } private func subscribeEventListeners() { @@ -209,7 +190,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { } else if mediaKey == .volumeDown { return .volumeUp } - return nil } } @@ -218,6 +198,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { extension AppDelegate: MediaKeyTapDelegate { func handle(mediaKey: MediaKey, event: KeyEvent?, modifiers: NSEvent.ModifierFlags?) { + 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 { + internalDisplay.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement) + return + } + } + let oppositeKey: MediaKey? = self.oppositeMediaKey(mediaKey: mediaKey) let isRepeat = event?.keyRepeat ?? false @@ -229,42 +220,40 @@ extension AppDelegate: MediaKeyTapDelegate { if isRepeat { return } - mediaKeyTimer.invalidate() } - let displays = self.displayManager?.getDisplays() ?? [Display]() - guard let currentDisplay = Utils.getCurrentDisplay(from: displays) else { return } + let displays = DisplayManager.shared.getAllDisplays() + guard let currentDisplay = DisplayManager.shared.getCurrentDisplay() else { return } let allDisplays = prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) ? displays : [currentDisplay] - let isSmallIncrement = modifiers?.isSuperset(of: NSEvent.ModifierFlags([.shift, .option])) ?? false // Introduce a small delay to handle the media key being held down let delay = isRepeat ? 0.05 : 0 - for display in allDisplays { - if (prefs.object(forKey: "\(display.identifier)-state") as? Bool) ?? true { + self.keyRepeatTimers[mediaKey] = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in + for display in allDisplays where display.isEnabled { switch mediaKey { case .brightnessUp, .brightnessDown: - self.keyRepeatTimers[mediaKey] = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in - let osdValue = display.calcNewValue(for: .brightness, isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement) - display.setBrightness(to: osdValue) - }) + display.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement) case .mute: // The mute key should not respond to press + hold if !isRepeat { - display.toggleMute() + // mute only matters for external displays + if let display = display as? ExternalDisplay { + display.toggleMute() + } } case .volumeUp, .volumeDown: - self.keyRepeatTimers[mediaKey] = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in - let osdValue = display.calcNewValue(for: .audioSpeakerVolume, isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement) - display.setVolume(to: osdValue) - }) + // volume only matters for external displays + if let display = display as? ExternalDisplay { + display.stepVolume(isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement) + } default: return } } - } + }) } // MARK: - Prefs notification @@ -304,7 +293,6 @@ extension AppDelegate: MediaKeyTapDelegate { let keysToDelete: [MediaKey] = [.volumeUp, .volumeDown, .mute] keys.removeAll { keysToDelete.contains($0) } } - self.mediaKeyTap?.stop() self.mediaKeyTap = MediaKeyTap(delegate: self, for: keys, observeBuiltIn: false) self.mediaKeyTap?.start() @@ -312,16 +300,13 @@ extension AppDelegate: MediaKeyTapDelegate { } extension AppDelegate: EventSubscriber { - /** - Fires off when the default audio device changes. - */ + /// Fires off when the default audio device changes. func eventReceiver(_ event: Event) { if case let .defaultOutputDeviceChanged(audioDevice)? = event as? AudioHardwareEvent { #if DEBUG os_log("Default output device changed to “%{public}@”.", type: .info, audioDevice.name) os_log("Can device set its own volume? %{public}@", type: .info, audioDevice.canSetVirtualMasterVolume(direction: .playback).description) #endif - self.startOrRestartMediaKeyTap() } } diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 702f29f..09f5897 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 574 + 638 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Manager/DisplayManager.swift b/MonitorControl/Manager/DisplayManager.swift index 383dd97..5a032df 100644 --- a/MonitorControl/Manager/DisplayManager.swift +++ b/MonitorControl/Manager/DisplayManager.swift @@ -1,6 +1,8 @@ -import Foundation +import Cocoa class DisplayManager { + public static let shared = DisplayManager() + private var displays: [Display] { didSet { NotificationCenter.default.post(name: Notification.Name(Utils.PrefKeys.displayListUpdate.rawValue), object: nil) @@ -15,10 +17,29 @@ class DisplayManager { self.displays = displays } - func getDisplays() -> [Display] { + func getAllDisplays() -> [Display] { return self.displays } + func getDdcCapableDisplays() -> [ExternalDisplay] { + return self.displays.compactMap { (display) -> ExternalDisplay? in + if let externalDisplay = display as? ExternalDisplay, externalDisplay.ddc != nil { + return externalDisplay + } else { return nil } + } + } + + func getBuiltInDisplay() -> Display? { + return self.displays.first { $0 is InternalDisplay } + } + + func getCurrentDisplay() -> Display? { + guard let mainDisplayID = NSScreen.main?.displayID else { + return nil + } + return self.displays.first { $0.identifier == mainDisplayID } + } + func addDisplay(display: Display) { self.displays.append(display) } diff --git a/MonitorControl/Model/Display.swift b/MonitorControl/Model/Display.swift new file mode 100644 index 0000000..05af8bb --- /dev/null +++ b/MonitorControl/Model/Display.swift @@ -0,0 +1,70 @@ +// +// Display.swift +// MonitorControl +// +// Created by Joni Van Roost on 24/01/2020. +// Copyright © 2020 Guillaume Broder. All rights reserved. +// + +import DDC +import Foundation + +class Display { + internal let identifier: CGDirectDisplayID + internal let name: String + internal var vendorNumber: UInt32? + internal var modelNumber: UInt32? + internal var isEnabled: Bool { + get { + return self.prefs.object(forKey: "\(self.identifier)-state") as? Bool ?? true + } + set { + self.prefs.set(newValue, forKey: "\(self.identifier)-state") + } + } + + private let prefs = UserDefaults.standard + + internal init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?) { + self.identifier = identifier + self.name = name + self.vendorNumber = vendorNumber + self.modelNumber = modelNumber + } + + func stepBrightness(isUp _: Bool, isSmallIncrement _: Bool) {} + + func setFriendlyName(_ value: String) { + self.prefs.set(value, forKey: "friendlyName-\(self.identifier)") + } + + func getFriendlyName() -> String { + return self.prefs.string(forKey: "friendlyName-\(self.identifier)") ?? self.name + } + + func showOsd(command: DDC.Command, value: Int, maxValue: Int = 100) { + guard let manager = OSDManager.sharedManager() as? OSDManager else { + return + } + + var osdImage: Int64! + switch command { + case .brightness: + osdImage = 1 // Brightness Image + case .audioSpeakerVolume: + osdImage = 3 // Speaker image + case .audioMuteScreenBlank: + osdImage = 4 // Mute image + default: + osdImage = 1 + } + + manager.showImage(osdImage, + onDisplayID: self.identifier, + priority: 0x1F4, + msecUntilFade: 1000, + filledChiclets: UInt32(value), + totalChiclets: UInt32(maxValue), + locked: false) + } +} diff --git a/MonitorControl/Display.swift b/MonitorControl/Model/ExternalDisplay.swift similarity index 86% rename from MonitorControl/Display.swift rename to MonitorControl/Model/ExternalDisplay.swift index 43bb1fe..468540c 100644 --- a/MonitorControl/Display.swift +++ b/MonitorControl/Model/ExternalDisplay.swift @@ -3,16 +3,14 @@ import Cocoa import DDC import os.log -class Display { - let identifier: CGDirectDisplayID - let name: String - let isBuiltin: Bool - var isEnabled: Bool +class ExternalDisplay: Display { var brightnessSliderHandler: SliderHandler? var volumeSliderHandler: SliderHandler? var contrastSliderHandler: SliderHandler? var ddc: DDC? + private let prefs = UserDefaults.standard + var hideOsd: Bool { get { return self.prefs.bool(forKey: "hideOsd-\(self.identifier)") @@ -33,17 +31,12 @@ class Display { } } - private let prefs = UserDefaults.standard private var audioPlayer: AVAudioPlayer? - private let osdChicletBoxes: Float = 16 - init(_ identifier: CGDirectDisplayID, name: String, isBuiltin: Bool, isEnabled: Bool = true) { - self.identifier = identifier - self.name = name - self.isEnabled = isBuiltin ? false : isEnabled + override init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?) { + super.init(identifier, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber) self.ddc = DDC(for: identifier) - self.isBuiltin = isBuiltin } // On some displays, the display's OSD overlaps the macOS OSD, @@ -97,7 +90,7 @@ class Display { if !fromVolumeSlider { self.hideDisplayOsd() - self.showOsd(command: .audioSpeakerVolume, value: volumeOSDValue) + self.showOsd(command: volumeOSDValue > 0 ? .audioSpeakerVolume : .audioMuteScreenBlank, value: volumeOSDValue) if volumeOSDValue > 0 { self.playVolumeChangedSound() @@ -109,8 +102,9 @@ class Display { } } - func setVolume(to volumeOSDValue: Int) { + func stepVolume(isUp: Bool, isSmallIncrement: Bool) { var muteValue: Int? + let volumeOSDValue = self.calcNewValue(for: .audioSpeakerVolume, isUp: isUp, isSmallIncrement: isSmallIncrement) let volumeDDCValue = UInt16(volumeOSDValue) if self.isMuted(), volumeOSDValue > 0 { @@ -134,7 +128,6 @@ class Display { return } } - self.saveValue(muteValue, for: .audioMuteScreenBlank) } @@ -154,7 +147,8 @@ class Display { } } - func setBrightness(to osdValue: Int) { + override func stepBrightness(isUp: Bool, isSmallIncrement: Bool) { + let osdValue = Int(self.calcNewValue(for: .brightness, isUp: isUp, isSmallIncrement: isSmallIncrement)) let isAlreadySet = osdValue == self.getValue(for: .brightness) let ddcValue = UInt16(osdValue) @@ -234,18 +228,17 @@ class Display { let filledChicletBoxes = self.osdChicletBoxes * (Float(currentValue) / Float(self.getMaxValue(for: command))) var nextFilledChicletBoxes: Float - var fillecChicletBoxesRel: Float = isUp ? 1 : -1 + var filledChicletBoxesRel: Float = isUp ? 1 : -1 // This is a workaround to ensure that if the user has set the value using a small step (that is, the current chiclet box isn't completely filled, // the next regular up or down step will only fill or empty that chiclet, and not the next one as well - it only really works because the max value is 100 if (isUp && ceil(filledChicletBoxes) - filledChicletBoxes > 0.15) || (!isUp && filledChicletBoxes - floor(filledChicletBoxes) > 0.15) { - fillecChicletBoxesRel = 0 + filledChicletBoxesRel = 0 } - nextFilledChicletBoxes = isUp ? ceil(filledChicletBoxes + fillecChicletBoxesRel) : floor(filledChicletBoxes + fillecChicletBoxesRel) + nextFilledChicletBoxes = isUp ? ceil(filledChicletBoxes + filledChicletBoxesRel) : floor(filledChicletBoxes + filledChicletBoxesRel) nextValue = Int(Float(self.getMaxValue(for: command)) * (nextFilledChicletBoxes / self.osdChicletBoxes)) } - return max(0, min(self.getMaxValue(for: command), Int(nextValue))) } @@ -263,7 +256,6 @@ class Display { func getMaxValue(for command: DDC.Command) -> Int { let max = self.prefs.integer(forKey: "max-\(command.rawValue)-\(self.identifier)") - return max == 0 ? 100 : max } @@ -275,14 +267,6 @@ class Display { self.prefs.set(value, forKey: "restore-\(command.rawValue)-\(self.identifier)") } - func setFriendlyName(_ value: String) { - self.prefs.set(value, forKey: "friendlyName-\(self.identifier)") - } - - func getFriendlyName() -> String { - return self.prefs.string(forKey: "friendlyName-\(self.identifier)") ?? self.name - } - func setPollingMode(_ value: Int) { self.prefs.set(String(value), forKey: "pollingMode-\(self.identifier)") } @@ -327,26 +311,8 @@ class Display { return isSmallIncrement ? 1 : Int(floor(Float(self.getMaxValue(for: command)) / self.osdChicletBoxes)) } - private func showOsd(command: DDC.Command, value: Int) { - guard let manager = OSDManager.sharedManager() as? OSDManager else { - return - } - - var osdImage: Int64 = 1 // Brightness Image - if command == .audioSpeakerVolume { - osdImage = 3 // Speaker image - if self.isMuted() { - osdImage = 4 // Mute speaker - } - } - - manager.showImage(osdImage, - onDisplayID: self.identifier, - priority: 0x1F4, - msecUntilFade: 1000, - filledChiclets: UInt32(value), - totalChiclets: UInt32(self.getMaxValue(for: command)), - locked: false) + override func showOsd(command: DDC.Command, value: Int, maxValue _: Int = 100) { + super.showOsd(command: command, value: value, maxValue: self.getMaxValue(for: command)) } private func supportsMuteCommand() -> Bool { diff --git a/MonitorControl/Model/InternalDisplay.swift b/MonitorControl/Model/InternalDisplay.swift new file mode 100644 index 0000000..bedc392 --- /dev/null +++ b/MonitorControl/Model/InternalDisplay.swift @@ -0,0 +1,85 @@ +// +// InternalDisplay.swift +// MonitorControl +// +// Created by Joni Van Roost on 24/01/2020. +// Copyright © 2020 Guillaume Broder. All rights reserved. +// +// Most of the code in this file was sourced from: +// https://github.com/fnesveda/ExternalDisplayBrightness +// all credit goes to @fnesveda + +import Foundation + +class InternalDisplay: Display { + // the queue for dispatching display operations, so they're not performed directly and concurrently + private var displayQueue: DispatchQueue + + override init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?) { + self.displayQueue = DispatchQueue(label: String("displayQueue-\(identifier)")) + super.init(identifier, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber) + } + + func calcNewBrightness(isUp: Bool, isSmallIncrement: Bool) -> Float { + var step: Float = (isUp ? 1 : -1) / 16.0 + let delta = step / 4 + if isSmallIncrement { + step = delta + } + return min(max(0, ceil((self.getBrightness() + delta) / step) * step), 1) + } + + public func getBrightness() -> Float { + self.displayQueue.sync { + Float(type(of: self).CoreDisplayGetUserBrightness?(self.identifier) ?? 0.5) + } + } + + override func stepBrightness(isUp: Bool, isSmallIncrement: Bool) { + let value = self.calcNewBrightness(isUp: isUp, isSmallIncrement: isSmallIncrement) + self.displayQueue.sync { + type(of: self).CoreDisplaySetUserBrightness?(self.identifier, Double(value)) + type(of: self).DisplayServicesBrightnessChanged?(self.identifier, Double(value)) + self.showOsd(command: .brightness, value: Int(value * 64), maxValue: 64) + } + } + + // notifies the system that the brightness of a specified display has changed (to update System Preferences etc.) + // unfortunately Apple doesn't provide a public API for this, so we have to manually extract the function from the DisplayServices framework + private static var DisplayServicesBrightnessChanged: ((CGDirectDisplayID, Double) -> Void)? { + let displayServicesPath = CFURLCreateWithString(kCFAllocatorDefault, "/System/Library/PrivateFrameworks/DisplayServices.framework" as CFString, nil) + if let displayServicesBundle = CFBundleCreate(kCFAllocatorDefault, displayServicesPath) { + if let funcPointer = CFBundleGetFunctionPointerForName(displayServicesBundle, "DisplayServicesBrightnessChanged" as CFString) { + typealias DSBCFunctionType = @convention(c) (UInt32, Double) -> Void + return unsafeBitCast(funcPointer, to: DSBCFunctionType.self) + } + } + return nil + } + + // reads the brightness of a display through the CoreDisplay framework + // unfortunately Apple doesn't provide a public API for this, so we have to manually extract the function from the CoreDisplay framework + private static var CoreDisplayGetUserBrightness: ((CGDirectDisplayID) -> Double)? { + let coreDisplayPath = CFURLCreateWithString(kCFAllocatorDefault, "/System/Library/Frameworks/CoreDisplay.framework" as CFString, nil) + if let coreDisplayBundle = CFBundleCreate(kCFAllocatorDefault, coreDisplayPath) { + if let funcPointer = CFBundleGetFunctionPointerForName(coreDisplayBundle, "CoreDisplay_Display_GetUserBrightness" as CFString) { + typealias CDGUBFunctionType = @convention(c) (UInt32) -> Double + return unsafeBitCast(funcPointer, to: CDGUBFunctionType.self) + } + } + return nil + } + + // sets the brightness of a display through the CoreDisplay framework + // unfortunately Apple doesn't provide a public API for this, so we have to manually extract the function from the CoreDisplay framework + private static var CoreDisplaySetUserBrightness: ((CGDirectDisplayID, Double) -> Void)? { + let coreDisplayPath = CFURLCreateWithString(kCFAllocatorDefault, "/System/Library/Frameworks/CoreDisplay.framework" as CFString, nil) + if let coreDisplayBundle = CFBundleCreate(kCFAllocatorDefault, coreDisplayPath) { + if let funcPointer = CFBundleGetFunctionPointerForName(coreDisplayBundle, "CoreDisplay_Display_SetUserBrightness" as CFString) { + typealias CDSUBFunctionType = @convention(c) (UInt32, Double) -> Void + return unsafeBitCast(funcPointer, to: CDSUBFunctionType.self) + } + } + return nil + } +} diff --git a/MonitorControl/Support/Utils.swift b/MonitorControl/Support/Utils.swift index 22826b7..b261b88 100644 --- a/MonitorControl/Support/Utils.swift +++ b/MonitorControl/Support/Utils.swift @@ -14,7 +14,7 @@ class Utils: NSObject { /// - command: Command (Brightness/Volume/...) /// - title: Title of the slider /// - Returns: An `NSSlider` slider - static func addSliderMenuItem(toMenu menu: NSMenu, forDisplay display: Display, command: DDC.Command, title: String) -> SliderHandler { + static func addSliderMenuItem(toMenu menu: NSMenu, forDisplay display: ExternalDisplay, command: DDC.Command, title: String) -> SliderHandler { let item = NSMenuItem() let handler = SliderHandler(display: display, command: command) @@ -131,28 +131,6 @@ class Utils: NSObject { } } - // MARK: - Display Infos - - /// Get the name of a display - /// - /// - Parameter edid: the EDID of a display - /// - Returns: a string - static func getDisplayName(forEdid edid: EDID) -> String { - return edid.displayName() ?? NSLocalizedString("Unknown", comment: "Unknown display name") - } - - /// Get the main display from a list of display - /// - /// - Parameter displays: List of Display - /// - Returns: the main display or nil if not found - static func getCurrentDisplay(from displays: [Display]) -> Display? { - guard let mainDisplayID = NSScreen.main?.displayID else { - return nil - } - - return displays.first { $0.identifier == mainDisplayID } - } - // MARK: - Enums /// UserDefault Keys for the app prefs diff --git a/MonitorControl/UI/Cells/ButtonCellView.swift b/MonitorControl/UI/Cells/ButtonCellView.swift index 42495a0..a0d59ff 100644 --- a/MonitorControl/UI/Cells/ButtonCellView.swift +++ b/MonitorControl/UI/Cells/ButtonCellView.swift @@ -4,7 +4,6 @@ import os.log class ButtonCellView: NSTableCellView { @IBOutlet var button: NSButton! var display: Display? - let prefs = UserDefaults.standard override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) @@ -12,17 +11,10 @@ class ButtonCellView: NSTableCellView { @IBAction func buttonToggled(_ sender: NSButton) { if let display = display { - switch sender.state { - case .on: - self.prefs.set(true, forKey: "\(display.identifier)-state") - case .off: - self.prefs.set(false, forKey: "\(display.identifier)-state") - default: - break - } - + let isEnabled = sender.state == .on + display.isEnabled = isEnabled #if DEBUG - os_log("Toggle enabled display state: %{public}@", type: .info, sender.state == .on ? "on" : "off") + os_log("Toggle enabled display state: %{public}@", type: .info, isEnabled ? "on" : "off") #endif } } diff --git a/MonitorControl/UI/Cells/FriendlyNameCellView.swift b/MonitorControl/UI/Cells/FriendlyNameCellView.swift index ad1866a..2792baf 100644 --- a/MonitorControl/UI/Cells/FriendlyNameCellView.swift +++ b/MonitorControl/UI/Cells/FriendlyNameCellView.swift @@ -22,7 +22,6 @@ class FriendlyNameCellView: NSTableCellView { !newValue.isEmpty { display.setFriendlyName(newValue) NotificationCenter.default.post(name: Notification.Name(Utils.PrefKeys.friendlyName.rawValue), object: nil) - #if DEBUG os_log("Value changed for friendly name: %{public}@", type: .info, "from `\(originalValue)` to `\(newValue)`") #endif diff --git a/MonitorControl/UI/Cells/HideOsdCellView.swift b/MonitorControl/UI/Cells/HideOsdCellView.swift index 5c983e5..704eed8 100644 --- a/MonitorControl/UI/Cells/HideOsdCellView.swift +++ b/MonitorControl/UI/Cells/HideOsdCellView.swift @@ -3,7 +3,7 @@ import os.log class HideOsdCellView: NSTableCellView { @IBOutlet var button: NSButton! - var display: Display? + var display: ExternalDisplay? let prefs = UserDefaults.standard override func draw(_ dirtyRect: NSRect) { diff --git a/MonitorControl/UI/Cells/LongerDelayCellView.swift b/MonitorControl/UI/Cells/LongerDelayCellView.swift index 857b3aa..1df916b 100644 --- a/MonitorControl/UI/Cells/LongerDelayCellView.swift +++ b/MonitorControl/UI/Cells/LongerDelayCellView.swift @@ -3,7 +3,7 @@ import os.log class LongerDelayCellView: NSTableCellView { @IBOutlet var button: NSButton! - var display: Display? + var display: ExternalDisplay? let prefs = UserDefaults.standard override func draw(_ dirtyRect: NSRect) { diff --git a/MonitorControl/UI/Cells/PollingCountCellView.swift b/MonitorControl/UI/Cells/PollingCountCellView.swift index de857c7..3da468e 100644 --- a/MonitorControl/UI/Cells/PollingCountCellView.swift +++ b/MonitorControl/UI/Cells/PollingCountCellView.swift @@ -2,7 +2,7 @@ import Cocoa import os.log class PollingCountCellView: NSTableCellView { - var display: Display? + var display: ExternalDisplay? @IBAction func valueChanged(_ sender: NSTextField) { if let display = display { diff --git a/MonitorControl/UI/Cells/PollingModeCellView.swift b/MonitorControl/UI/Cells/PollingModeCellView.swift index 0d800dd..cfc7976 100644 --- a/MonitorControl/UI/Cells/PollingModeCellView.swift +++ b/MonitorControl/UI/Cells/PollingModeCellView.swift @@ -11,7 +11,7 @@ import os.log We use these tags as a way to mark selection */ class PollingModeCellView: NSTableCellView { - var display: Display? + var display: ExternalDisplay? @IBOutlet var pollingModeMenu: NSPopUpButtonCell! var didChangePollingMode: ((_ pollingModeInt: Int) -> Void)? diff --git a/MonitorControl/UI/SliderHandler.swift b/MonitorControl/UI/SliderHandler.swift index d5a8c59..362c06f 100644 --- a/MonitorControl/UI/SliderHandler.swift +++ b/MonitorControl/UI/SliderHandler.swift @@ -3,10 +3,10 @@ import DDC class SliderHandler { var slider: NSSlider? - var display: Display + var display: ExternalDisplay let cmd: DDC.Command - public init(display: Display, command: DDC.Command) { + public init(display: ExternalDisplay, command: DDC.Command) { self.display = display self.cmd = command } diff --git a/MonitorControl/View Controllers/AdvancedPrefsViewController.swift b/MonitorControl/View Controllers/AdvancedPrefsViewController.swift index 70912ce..7837c32 100644 --- a/MonitorControl/View Controllers/AdvancedPrefsViewController.swift +++ b/MonitorControl/View Controllers/AdvancedPrefsViewController.swift @@ -9,8 +9,7 @@ class AdvancedPrefsViewController: NSViewController, MASPreferencesViewControlle var toolbarItemImage: NSImage? = NSImage(named: NSImage.advancedName) let prefs = UserDefaults.standard - var displays: [Display] = [] - var displayManager: DisplayManager? + var displays: [ExternalDisplay] = [] enum DisplayColumn: Int { case friendlyName @@ -61,10 +60,8 @@ class AdvancedPrefsViewController: NSViewController, MASPreferencesViewControlle } @objc func loadDisplayList() { - if let displays = displayManager?.getDisplays() { - self.displays = displays - self.displayList.reloadData() - } + self.displays = DisplayManager.shared.getDdcCapableDisplays() + self.displayList.reloadData() } func numberOfRows(in _: NSTableView) -> Int { @@ -119,7 +116,7 @@ class AdvancedPrefsViewController: NSViewController, MASPreferencesViewControlle return nil } - private func getText(for column: DisplayColumn, with display: Display) -> String { + private func getText(for column: DisplayColumn, with display: ExternalDisplay) -> String { switch column { case .friendlyName: return display.getFriendlyName() diff --git a/MonitorControl/View Controllers/DisplayPrefsViewController.swift b/MonitorControl/View Controllers/DisplayPrefsViewController.swift index 7ae03c8..9fdc84a 100644 --- a/MonitorControl/View Controllers/DisplayPrefsViewController.swift +++ b/MonitorControl/View Controllers/DisplayPrefsViewController.swift @@ -10,7 +10,6 @@ class DisplayPrefsViewController: NSViewController, MASPreferencesViewController let prefs = UserDefaults.standard var displays: [Display] = [] - var displayManager: DisplayManager? enum DisplayColumn: Int { case checkbox @@ -57,9 +56,7 @@ class DisplayPrefsViewController: NSViewController, MASPreferencesViewController // MARK: - Table datasource @objc func loadDisplayList() { - if let displays = self.displayManager?.getDisplays() { - self.displays = displays - } + self.displays = DisplayManager.shared.getAllDisplays() self.displayList.reloadData() } @@ -82,7 +79,6 @@ class DisplayPrefsViewController: NSViewController, MASPreferencesViewController if let cell = tableView.makeView(withIdentifier: tableColumn.identifier, owner: nil) as? ButtonCellView { cell.display = display cell.button.state = display.isEnabled ? .on : .off - cell.button.isEnabled = !display.isBuiltin return cell } case .ddc: diff --git a/MonitorControl/View Controllers/KeysPrefsViewController.swift b/MonitorControl/View Controllers/KeysPrefsViewController.swift index eac40df..7f8308a 100644 --- a/MonitorControl/View Controllers/KeysPrefsViewController.swift +++ b/MonitorControl/View Controllers/KeysPrefsViewController.swift @@ -17,11 +17,9 @@ class KeysPrefsViewController: NSViewController, MASPreferencesViewController { @IBAction func listenForChanged(_ sender: NSPopUpButton) { self.prefs.set(sender.selectedTag(), forKey: Utils.PrefKeys.listenFor.rawValue) - #if DEBUG os_log("Toggle keys listened for state state: %{public}@", type: .info, sender.selectedItem?.title ?? "") #endif - NotificationCenter.default.post(name: Notification.Name(Utils.PrefKeys.listenFor.rawValue), object: nil) } } diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index 0d1eb77..9bd4752 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 574 + 638 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly