diff --git a/.github/menudisplay.png b/.github/menudisplay.png new file mode 100644 index 00000000..ac9c38c2 Binary files /dev/null and b/.github/menudisplay.png differ diff --git a/.github/menugeneral.png b/.github/menugeneral.png new file mode 100644 index 00000000..d83ba349 Binary files /dev/null and b/.github/menugeneral.png differ diff --git a/.github/menukeys.png b/.github/menukeys.png new file mode 100644 index 00000000..63d2cf14 Binary files /dev/null and b/.github/menukeys.png differ diff --git a/.github/menulet-outdated.png b/.github/menulet-outdated.png new file mode 100644 index 00000000..36ab2c1e Binary files /dev/null and b/.github/menulet-outdated.png differ diff --git a/.github/menulet.png b/.github/menulet.png index 36ab2c1e..1e360679 100644 Binary files a/.github/menulet.png and b/.github/menulet.png differ diff --git a/.github/osd.jpg b/.github/osd.jpg new file mode 100644 index 00000000..62bc7517 Binary files /dev/null and b/.github/osd.jpg differ diff --git a/.github/osd.png b/.github/osd.png deleted file mode 100644 index 584ad265..00000000 Binary files a/.github/osd.png and /dev/null differ diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj index 92ec6356..7fe33595 100644 --- a/MonitorControl.xcodeproj/project.pbxproj +++ b/MonitorControl.xcodeproj/project.pbxproj @@ -12,12 +12,12 @@ 56754EAF1D9A4016007BCDC5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */; }; 56754EB11D9A4016007BCDC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB01D9A4016007BCDC5 /* Assets.xcassets */; }; 56754EB41D9A4016007BCDC5 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB21D9A4016007BCDC5 /* MainMenu.xib */; }; + 6C778D5A21E91060000A4D5F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6C778D5821E91060000A4D5F /* Main.storyboard */; }; 9A19D3B73485870616B6D4E0 /* Pods_MonitorControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */; }; F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* Display.swift */; }; F0445D3820023E710025AE82 /* MainPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D3720023E710025AE82 /* MainPrefsViewController.swift */; }; F0445D3D200254FA0025AE82 /* KeysPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D3B200254FA0025AE82 /* KeysPrefsViewController.swift */; }; F0445D40200259C10025AE82 /* DisplayPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D3F200259C10025AE82 /* DisplayPrefsViewController.swift */; }; - F0445D41200282E60025AE82 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F0445D43200282E60025AE82 /* Main.storyboard */; }; F0445D4D200294AB0025AE82 /* ButtonCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D4C200294AB0025AE82 /* ButtonCellView.swift */; }; F06792EA200A73460066C438 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06792E9200A73460066C438 /* AppDelegate.swift */; }; F06792F6200A745F0066C438 /* MonitorControlHelper.app in [Login] Copy Helper to start at Login */ = {isa = PBXBuildFile; fileRef = F06792E7200A73460066C438 /* MonitorControlHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -56,13 +56,13 @@ 56754EB01D9A4016007BCDC5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56754EB31D9A4016007BCDC5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 56754EB51D9A4016007BCDC5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6C778D5921E91060000A4D5F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 6C778D5E21E910A2000A4D5F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; + 6C778D5F21E910A6000A4D5F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; F03A8DF11FFBAA6F0034DC27 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.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 = ""; }; F0445D3F200259C10025AE82 /* DisplayPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPrefsViewController.swift; sourceTree = ""; }; - F0445D42200282E60025AE82 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - F0445D45200282EB0025AE82 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; - F0445D47200282F80025AE82 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; F0445D49200285690025AE82 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; F0445D4B2002856C0025AE82 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/MainMenu.strings; sourceTree = ""; }; F0445D4C200294AB0025AE82 /* ButtonCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCellView.swift; sourceTree = ""; }; @@ -149,7 +149,7 @@ children = ( 56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */, F091C9B71F6EA79B0096FD65 /* Utils.swift */, - F0445D43200282E60025AE82 /* Main.storyboard */, + 6C778D5821E91060000A4D5F /* Main.storyboard */, 56754EB21D9A4016007BCDC5 /* MainMenu.xib */, F0445D3620023D5B0025AE82 /* Prefs */, F091C9B41F6EA6180096FD65 /* Objects */, @@ -247,6 +247,7 @@ 56754EA91D9A4016007BCDC5 /* Resources */, 9DD5968596EFAF0E2EB56496 /* [CP] Embed Pods Frameworks */, F06792F5200A73FA0066C438 /* [Login] Copy Helper to start at Login */, + 6C778D4D21E90DA1000A4D5F /* ShellScript */, ); buildRules = ( ); @@ -286,13 +287,14 @@ TargetAttributes = { 56754EAA1D9A4016007BCDC5 = { CreatedOnToolsVersion = 8.0; - DevelopmentTeam = KGY56RWR9A; - LastSwiftMigration = 0900; + DevelopmentTeam = CYC8C8R4K9; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; }; F06792E6200A73460066C438 = { CreatedOnToolsVersion = 9.2; - DevelopmentTeam = KGY56RWR9A; + DevelopmentTeam = CYC8C8R4K9; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; }; }; @@ -323,8 +325,8 @@ buildActionMask = 2147483647; files = ( 56754EB11D9A4016007BCDC5 /* Assets.xcassets in Resources */, - F0445D41200282E60025AE82 /* Main.storyboard in Resources */, F0EB972F1F6ED7C800686D2A /* Localizable.strings in Resources */, + 6C778D5A21E91060000A4D5F /* Main.storyboard in Resources */, 55359E3B1E2737EC002671BC /* ddcctl.sh in Resources */, 56754EB41D9A4016007BCDC5 /* MainMenu.xib in Resources */, ); @@ -340,6 +342,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 6C778D4D21E90DA1000A4D5F /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nbuildNumber=$(/usr/libexec/PlistBuddy -c \"Print CFBundleVersion\" \"${INFOPLIST_FILE}\")\nbuildNumber=$(($buildNumber + 1))\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"${INFOPLIST_FILE}\"\n\n"; + }; 9DD5968596EFAF0E2EB56496 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -347,11 +366,13 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/AMCoreAudio/AMCoreAudio.framework", "${BUILT_PRODUCTS_DIR}/MASPreferences/MASPreferences.framework", "${BUILT_PRODUCTS_DIR}/MediaKeyTap/MediaKeyTap.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AMCoreAudio.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MASPreferences.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MediaKeyTap.framework", ); @@ -432,12 +453,12 @@ name = MainMenu.xib; sourceTree = ""; }; - F0445D43200282E60025AE82 /* Main.storyboard */ = { + 6C778D5821E91060000A4D5F /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( - F0445D42200282E60025AE82 /* Base */, - F0445D45200282EB0025AE82 /* fr */, - F0445D47200282F80025AE82 /* en */, + 6C778D5921E91060000A4D5F /* Base */, + 6C778D5E21E910A2000A4D5F /* en */, + 6C778D5F21E910A6000A4D5F /* fr */, ); name = Main.storyboard; sourceTree = ""; @@ -600,14 +621,14 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = KGY56RWR9A; + DEVELOPMENT_TEAM = CYC8C8R4K9; INFOPLIST_FILE = "$(SRCROOT)/MonitorControl/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl/Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Debug; }; @@ -619,14 +640,14 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = KGY56RWR9A; + DEVELOPMENT_TEAM = CYC8C8R4K9; INFOPLIST_FILE = "$(SRCROOT)/MonitorControl/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl/Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Release; }; @@ -641,7 +662,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = KGY56RWR9A; + DEVELOPMENT_TEAM = CYC8C8R4K9; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = MonitorControlHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; @@ -649,7 +670,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControlHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Debug; }; @@ -664,7 +685,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = KGY56RWR9A; + DEVELOPMENT_TEAM = CYC8C8R4K9; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = MonitorControlHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; @@ -672,7 +693,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControlHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Release; }; diff --git a/MonitorControl.xcodeproj/xcshareddata/xcschemes/MonitorControl.xcscheme b/MonitorControl.xcodeproj/xcshareddata/xcschemes/MonitorControl.xcscheme new file mode 100644 index 00000000..eaaeab23 --- /dev/null +++ b/MonitorControl.xcodeproj/xcshareddata/xcschemes/MonitorControl.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MonitorControl/AppDelegate.swift b/MonitorControl/AppDelegate.swift index 9a8401e1..e4b7de53 100644 --- a/MonitorControl/AppDelegate.swift +++ b/MonitorControl/AppDelegate.swift @@ -11,12 +11,13 @@ import Cocoa import Foundation import MediaKeyTap import MASPreferences +import AMCoreAudio var app: AppDelegate! = nil let prefs = UserDefaults.standard @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate { +class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var statusMenu: NSMenu! @IBOutlet weak var window: NSWindow! @@ -26,122 +27,97 @@ class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate { var monitorItems: [NSMenuItem] = [] var displays: [Display] = [] - let step = 100/16 + let step = 100/16 - var mediaKeyTap: MediaKeyTap? - var prefsController: NSWindowController? + var mediaKeyTap: MediaKeyTap? + var prefsController: NSWindowController? - var keysListenedFor: [MediaKey] = [.brightnessUp, .brightnessDown, .mute, .volumeUp, .volumeDown] + var keysListenedFor: [MediaKey] = [.brightnessUp, .brightnessDown, .mute, .volumeUp, .volumeDown] func applicationDidFinishLaunching(_ aNotification: Notification) { app = self - let listenFor = prefs.integer(forKey: Utils.PrefKeys.listenFor.rawValue) - if listenFor == Utils.ListenForKeys.brightnessOnlyKeys.rawValue { - keysListenedFor.removeSubrange(2...4) - } else if listenFor == Utils.ListenForKeys.volumeOnlyKeys.rawValue { - keysListenedFor.removeSubrange(0...1) - } - - mediaKeyTap = MediaKeyTap.init(delegate: self, for: keysListenedFor, observeBuiltIn: false) - let storyboard: NSStoryboard = NSStoryboard.init(name: NSStoryboard.Name(rawValue: "Main"), bundle: Bundle.main) - let views = [ - storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "MainPrefsVC")), - storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "KeysPrefsVC")), - storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "DisplayPrefsVC")) - ] - prefsController = MASPreferencesWindowController(viewControllers: views, title: NSLocalizedString("Preferences", comment: "Shown in Preferences window")) - - NotificationCenter.default.addObserver(self, selector: #selector(handleListenForChanged), name: NSNotification.Name.init(Utils.PrefKeys.listenFor.rawValue), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleShowContrastChanged), name: NSNotification.Name.init(Utils.PrefKeys.showContrast.rawValue), object: nil) - - statusItem.image = NSImage.init(named: NSImage.Name(rawValue: "status")) + setupLayout() + subscribeEventListeners() + setVolumeKeysMode() + statusItem.image = NSImage.init(named: "status") statusItem.menu = statusMenu - - setDefaultPrefs() - + setDefaultPrefs() Utils.acquirePrivileges() - CGDisplayRegisterReconfigurationCallback({_, _, _ in app.updateDisplays()}, nil) updateDisplays() - - mediaKeyTap?.start() } - func applicationWillTerminate(_ aNotification: Notification) { - } - - @IBAction func quitClicked(_ sender: AnyObject) { - NSApplication.shared.terminate(self) - } - - @IBAction func prefsClicked(_ sender: AnyObject) { - if let prefsController = prefsController { - prefsController.showWindow(sender) - NSApp.activate(ignoringOtherApps: true) - prefsController.window?.makeKeyAndOrderFront(sender) - } - } - - /// Set the default prefs of the app - func setDefaultPrefs() { - let prefs = UserDefaults.standard - if !prefs.bool(forKey: Utils.PrefKeys.appAlreadyLaunched.rawValue) { - prefs.set(true, forKey: Utils.PrefKeys.appAlreadyLaunched.rawValue) + @IBAction func quitClicked(_ sender: AnyObject) { + NSApplication.shared.terminate(self) + } - prefs.set(false, forKey: Utils.PrefKeys.startAtLogin.rawValue) + @IBAction func prefsClicked(_ sender: AnyObject) { + if let prefsController = prefsController { + prefsController.showWindow(sender) + NSApp.activate(ignoringOtherApps: true) + prefsController.window?.makeKeyAndOrderFront(sender) + } + } - prefs.set(false, forKey: Utils.PrefKeys.showContrast.rawValue) - prefs.set(false, forKey: Utils.PrefKeys.lowerContrast.rawValue) - } - } + /// Set the default prefs of the app + func setDefaultPrefs() { + let prefs = UserDefaults.standard + if !prefs.bool(forKey: Utils.PrefKeys.appAlreadyLaunched.rawValue) { + prefs.set(true, forKey: Utils.PrefKeys.appAlreadyLaunched.rawValue) - // MARK: - Menu + prefs.set(false, forKey: Utils.PrefKeys.startAtLogin.rawValue) - func clearDisplays() { - if statusMenu.items.count > 2 { - var items: [NSMenuItem] = [] - for i in 0.. 2 { + var items: [NSMenuItem] = [] + for i in 0.. Bool in - if let id = screen.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as? CGDirectDisplayID { - // Is Built In Screen (e.g. MBP/iMac Screen) - if CGDisplayIsBuiltin(id) != 0 { - return false - } - - // Does screen support EDID ? - var edid = EDID() - if !EDIDTest(id, &edid) { - return false - } - - return true - } - return false - } - - if filteredScreens.count == 1 { - self.addScreenToMenu(screen: filteredScreens[0], asSubMenu: false) - } else { - for screen in filteredScreens { - self.addScreenToMenu(screen: screen, asSubMenu: true) - } - } + clearDisplays() + + var filteredScreens = NSScreen.screens.filter { screen -> Bool in + if let id = screen.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as? CGDirectDisplayID { + // Is Built In Screen (e.g. MBP/iMac Screen) + if CGDisplayIsBuiltin(id) != 0 { + return false + } + + // Does screen support EDID ? + var edid = EDID() + if !EDIDTest(id, &edid) { + return false + } + + return true + } + return false + } + + if filteredScreens.count == 1 { + self.addScreenToMenu(screen: filteredScreens[0], asSubMenu: false) + } else { + for screen in filteredScreens { + self.addScreenToMenu(screen: screen, asSubMenu: true) + } + } if filteredScreens.count == 0 { // If no DDC capable display was detected @@ -153,102 +129,175 @@ class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate { } } - /// 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) { - if let id = screen.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as? CGDirectDisplayID { - - var edid = EDID() - if EDIDTest(id, &edid) { - let name = Utils.getDisplayName(forEdid: edid) - let serial = Utils.getDisplaySerial(forEdid: edid) - - let display = Display.init(id, name: name, serial: serial) - - let monitorSubMenu: NSMenu = asSubMenu ? NSMenu() : statusMenu - let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, - forDisplay: display, - command: AUDIO_SPEAKER_VOLUME, - 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 - displays.append(display) - - let monitorMenuItem = NSMenuItem() - monitorMenuItem.title = "\(name)" - if asSubMenu { - monitorMenuItem.submenu = monitorSubMenu - } - - monitorItems.append(monitorMenuItem) - statusMenu.insertItem(monitorMenuItem, at: 0) - } - } - } - - // MARK: - Media Key Tap delegate - - func handle(mediaKey: MediaKey, event: KeyEvent?) { - guard let currentDisplay = Utils.getCurrentDisplay(from: displays) else { return } - let allDisplays = prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) ? displays : [currentDisplay] - for display in allDisplays { - if (prefs.object(forKey: "\(display.identifier)-state") as? Bool) ?? true { - switch mediaKey { - case .brightnessUp: - let value = display.calcNewValue(for: BRIGHTNESS, withRel: +step) - display.setBrightness(to: value) - case .brightnessDown: - let value = currentDisplay.calcNewValue(for: BRIGHTNESS, withRel: -step) - display.setBrightness(to: value) - case .mute: - display.mute() - case .volumeUp: - let value = display.calcNewValue(for: AUDIO_SPEAKER_VOLUME, withRel: +step) - display.setVolume(to: value) - case .volumeDown: - let value = display.calcNewValue(for: AUDIO_SPEAKER_VOLUME, withRel: -step) - display.setVolume(to: value) - default: - return - } - } - } - - } - - // MARK: - Prefs notification - - @objc func handleListenForChanged() { - let listenFor = prefs.integer(forKey: Utils.PrefKeys.listenFor.rawValue) - keysListenedFor = [.brightnessUp, .brightnessDown, .mute, .volumeUp, .volumeDown] - if listenFor == Utils.ListenForKeys.brightnessOnlyKeys.rawValue { - keysListenedFor.removeSubrange(2...4) - } else if listenFor == Utils.ListenForKeys.volumeOnlyKeys.rawValue { - keysListenedFor.removeSubrange(0...1) - } - - mediaKeyTap?.stop() - mediaKeyTap = MediaKeyTap.init(delegate: self, for: keysListenedFor, observeBuiltIn: false) - mediaKeyTap?.start() - } - - @objc func handleShowContrastChanged() { - self.updateDisplays() - } + /// 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) { + if let id = screen.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as? CGDirectDisplayID { + + var edid = EDID() + if EDIDTest(id, &edid) { + let name = Utils.getDisplayName(forEdid: edid) + let serial = Utils.getDisplaySerial(forEdid: edid) + + let display = Display.init(id, name: name, serial: serial) + + let monitorSubMenu: NSMenu = asSubMenu ? NSMenu() : statusMenu + let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, + forDisplay: display, + command: AUDIO_SPEAKER_VOLUME, + 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 + displays.append(display) + + let monitorMenuItem = NSMenuItem() + monitorMenuItem.title = "\(name)" + if asSubMenu { + monitorMenuItem.submenu = monitorSubMenu + } + + monitorItems.append(monitorMenuItem) + statusMenu.insertItem(monitorMenuItem, at: 0) + } + } + } + + private func setupLayout() { + let storyboard: NSStoryboard = NSStoryboard.init(name: "Main", bundle: Bundle.main) + let views = [ + storyboard.instantiateController(withIdentifier: "MainPrefsVC"), + storyboard.instantiateController(withIdentifier: "KeysPrefsVC"), + storyboard.instantiateController(withIdentifier: "DisplayPrefsVC") + ] + prefsController = MASPreferencesWindowController(viewControllers: views, title: NSLocalizedString("Preferences", comment: "Shown in Preferences window")) + } + + private func subscribeEventListeners() { + // subscribe KeyTap event listener + NotificationCenter.default.addObserver(self, selector: #selector(handleListenForChanged), name: NSNotification.Name.init(Utils.PrefKeys.listenFor.rawValue), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleShowContrastChanged), name: NSNotification.Name.init(Utils.PrefKeys.showContrast.rawValue), object: nil) + + // subscribe Audio output detector (AMCoreAudio) + NotificationCenter.defaultCenter.subscribe(self, eventType: AudioHardwareEvent.self, dispatchQueue: DispatchQueue.main) + } +} + +// MARK: - Media Key Tap delegate +extension AppDelegate: MediaKeyTapDelegate { + + func handle(mediaKey: MediaKey, event: KeyEvent?) { + + guard let currentDisplay = Utils.getCurrentDisplay(from: displays) else { return } + let allDisplays = prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) ? displays : [currentDisplay] + + for display in allDisplays { + if (prefs.object(forKey: "\(display.identifier)-state") as? Bool) ?? true { + switch mediaKey { + case .brightnessUp: + let value = display.calcNewValue(for: BRIGHTNESS, withRel: +step) + display.setBrightness(to: value) + case .brightnessDown: + let value = currentDisplay.calcNewValue(for: BRIGHTNESS, withRel: -step) + display.setBrightness(to: value) + case .mute: + display.mute() + case .volumeUp: + let value = display.calcNewValue(for: AUDIO_SPEAKER_VOLUME, withRel: +step) + display.setVolume(to: value) + case .volumeDown: + let value = display.calcNewValue(for: AUDIO_SPEAKER_VOLUME, withRel: -step) + display.setVolume(to: value) + + default: + return + } + } + } + + } + + // MARK: - Prefs notification + @objc func handleListenForChanged() { + readKeyListenPreferences() + setKeysToListenFor() + } + + @objc func handleShowContrastChanged() { + self.updateDisplays() + } + + private func setKeysToListenFor() { + mediaKeyTap?.stop() + mediaKeyTap = MediaKeyTap.init(delegate: self, for: keysListenedFor, observeBuiltIn: false) + mediaKeyTap?.start() + } + + private func readKeyListenPreferences() { + let listenFor = prefs.integer(forKey: Utils.PrefKeys.listenFor.rawValue) + keysListenedFor = [.brightnessUp, .brightnessDown, .mute, .volumeUp, .volumeDown] + if listenFor == Utils.ListenForKeys.brightnessOnlyKeys.rawValue { + keysListenedFor.removeSubrange(2...4) + } else if listenFor == Utils.ListenForKeys.volumeOnlyKeys.rawValue { + keysListenedFor.removeSubrange(0...1) + } + } +} + +extension AppDelegate: EventSubscriber { + + /** + Fires off when a change in default audio device is detected. + */ + func eventReceiver(_ event: Event) { + + switch event { + case let event as AudioHardwareEvent: + switch event { + case .defaultOutputDeviceChanged(let audioDevice): + #if DEBUG + print("Default output device changed to \(audioDevice)") + print("Can device set its own volume? \(audioDevice.canSetVirtualMasterVolume(direction: .playback))") + #endif + setVolumeKeysMode() + default: break + } + default: break + } + } + /** + We check if the current default audio output device can change the volume, + if not, we know for sure that we don't need to interact with it. + */ + func setVolumeKeysMode() { + + readKeyListenPreferences() + + if let defaultOutputDevice = AudioDevice.defaultOutputDevice() { + if defaultOutputDevice.canSetVirtualMasterVolume(direction: .playback) { + // Remove volume related keys + let keysToDelete: [MediaKey] = [.volumeUp, .volumeDown, .mute] + keysListenedFor = keysListenedFor.filter({ !keysToDelete.contains($0) }) + } else { + // load keys to listen to from prefs like normal + readKeyListenPreferences() + } + } + setKeysToListenFor() + } } diff --git a/MonitorControl/Base.lproj/Main.storyboard b/MonitorControl/Base.lproj/Main.storyboard index f133628c..8c520638 100644 --- a/MonitorControl/Base.lproj/Main.storyboard +++ b/MonitorControl/Base.lproj/Main.storyboard @@ -1,85 +1,11 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -89,7 +15,7 @@ - + @@ -177,7 +103,7 @@ - + @@ -208,7 +134,7 @@ - + @@ -241,7 +167,7 @@ - + @@ -282,11 +208,11 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 5334618d..874975af 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.1 + 1.3.0 CFBundleVersion - 45 + 48 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Prefs/DisplayPrefsViewController.swift b/MonitorControl/Prefs/DisplayPrefsViewController.swift index 1f1a46eb..3b933c7d 100644 --- a/MonitorControl/Prefs/DisplayPrefsViewController.swift +++ b/MonitorControl/Prefs/DisplayPrefsViewController.swift @@ -13,7 +13,7 @@ class DisplayPrefsViewController: NSViewController, MASPreferencesViewController var viewIdentifier: String = "Display" var toolbarItemLabel: String? = NSLocalizedString("Display", comment: "Shown in the main prefs window") - var toolbarItemImage: NSImage? = NSImage.init(named: .computer) + var toolbarItemImage: NSImage? = NSImage.init(named: NSImage.computerName) let prefs = UserDefaults.standard var displays: [Display] = [] diff --git a/MonitorControl/Prefs/KeysPrefsViewController.swift b/MonitorControl/Prefs/KeysPrefsViewController.swift index c78b943a..9c74dbfb 100644 --- a/MonitorControl/Prefs/KeysPrefsViewController.swift +++ b/MonitorControl/Prefs/KeysPrefsViewController.swift @@ -13,7 +13,7 @@ class KeysPrefsViewController: NSViewController, MASPreferencesViewController { var viewIdentifier: String = "Keys" var toolbarItemLabel: String? = NSLocalizedString("Keys", comment: "Shown in the main prefs window") - var toolbarItemImage: NSImage? = NSImage.init(named: NSImage.Name.init("KeyboardPref")) + var toolbarItemImage: NSImage? = NSImage.init(named: "KeyboardPref") let prefs = UserDefaults.standard @IBOutlet var listenFor: NSPopUpButton! diff --git a/MonitorControl/Prefs/MainPrefsViewController.swift b/MonitorControl/Prefs/MainPrefsViewController.swift index 322ddca5..7805f86b 100644 --- a/MonitorControl/Prefs/MainPrefsViewController.swift +++ b/MonitorControl/Prefs/MainPrefsViewController.swift @@ -14,10 +14,12 @@ class MainPrefsViewController: NSViewController, MASPreferencesViewController { var viewIdentifier: String = "Main" var toolbarItemLabel: String? = NSLocalizedString("General", comment: "Shown in the main prefs window") - var toolbarItemImage: NSImage? = NSImage.init(named: .preferencesGeneral) + var toolbarItemImage: NSImage? = NSImage.init(named: NSImage.preferencesGeneralName) let prefs = UserDefaults.standard - @IBOutlet var startAtLogin: NSButton! + + @IBOutlet weak var versionLabel: NSTextField! + @IBOutlet var startAtLogin: NSButton! @IBOutlet var showContrastSlider: NSButton! @IBOutlet var lowerContrast: NSButton! @@ -27,6 +29,7 @@ class MainPrefsViewController: NSViewController, MASPreferencesViewController { startAtLogin.state = prefs.bool(forKey: Utils.PrefKeys.startAtLogin.rawValue) ? .on : .off showContrastSlider.state = prefs.bool(forKey: Utils.PrefKeys.showContrast.rawValue) ? .on : .off lowerContrast.state = prefs.bool(forKey: Utils.PrefKeys.lowerContrast.rawValue) ? .on : .off + setVersionNumber() } @IBAction func startAtLoginClicked(_ sender: NSButton) { @@ -75,4 +78,10 @@ class MainPrefsViewController: NSViewController, MASPreferencesViewController { print("Toggle lower contrast after brightness state -> \(sender.state == .on ? "on" : "off")") #endif } + + fileprivate func setVersionNumber() { + let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") ?? "unknown" + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") ?? "unknown" + versionLabel.stringValue = "version \(versionNumber) build \(buildNumber)" + } } diff --git a/MonitorControl/Utils.swift b/MonitorControl/Utils.swift index abdc81ac..87bf2cee 100644 --- a/MonitorControl/Utils.swift +++ b/MonitorControl/Utils.swift @@ -112,13 +112,9 @@ class Utils: NSObject { DispatchQueue.global(qos: .background).async { var val: Int? - for _ in 0...100 { - if let res = getCommand(command, fromMonitor: display.identifier) { - val = res - break - } - usleep(40000) - } + if let res = getCommand(command, fromMonitor: display.identifier) { + val = res + } if let val = val { display.saveValue(val, for: command) @@ -128,7 +124,6 @@ class Utils: NSObject { } } } - return handler } diff --git a/Podfile b/Podfile index ccf47fc8..bc304895 100644 --- a/Podfile +++ b/Podfile @@ -7,4 +7,5 @@ target 'MonitorControl' do pod 'MediaKeyTap', :git => 'https://github.com/the0neyouseek/MediaKeyTap.git' pod 'MASPreferences' + pod 'AMCoreAudio', '~> 3.2' end diff --git a/Podfile.lock b/Podfile.lock index 73d38858..8cbe4ded 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,13 +1,16 @@ PODS: + - AMCoreAudio (3.2.1) - MASPreferences (1.3) - MediaKeyTap (2.1.0) DEPENDENCIES: + - AMCoreAudio (~> 3.2) - MASPreferences - MediaKeyTap (from `https://github.com/the0neyouseek/MediaKeyTap.git`) SPEC REPOS: - https://github.com/CocoaPods/Specs.git: + https://github.com/cocoapods/specs.git: + - AMCoreAudio - MASPreferences EXTERNAL SOURCES: @@ -16,13 +19,14 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: MediaKeyTap: - :commit: 2ff6e582c0c03b735d24d0800f4e43611d316e93 + :commit: 3722ad54585d931977af8152a9555e832f4000f6 :git: https://github.com/the0neyouseek/MediaKeyTap.git SPEC CHECKSUMS: + AMCoreAudio: 7fa6b718dc93acc29f849d60c3ad680ae1bf07b5 MASPreferences: c08b8622dd17b47da87669e741efd7c92e970e8c MediaKeyTap: b652877e9ae2d52ca4f5310fa5152945ad3f0798 -PODFILE CHECKSUM: efa14c79ecdfeaf061bbc8ed950ee540d77f987a +PODFILE CHECKSUM: 1b20d86a04731d82d4e8f346646d2b6b9d2db778 -COCOAPODS: 1.5.0 +COCOAPODS: 1.5.3 diff --git a/README.md b/README.md index c1a2f330..9d75c893 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,45 @@ -# MonitorControl - -Control your external monitor brightness, contrast or volume directly from a menulet or with keyboard native keys - -![MonitorControl menulet](./.github/menulet.png) - -*Bonus: Using keyboard keys display the native osd :* - -![MonitorControl OSD](./.github/osd.png) - +

MonitorControl

+ + +
+Control your external monitor brightness, contrast or volume directly from a menulet or with keyboard native keys. +
+ +
+ + +
+ + + downloads + + + + latest version + + + + license + + + + platform + +
+ +
+ +
+ menulet screenshot +

+ general screenshotkeys screenshotdisplay screenshot + +
+ +*Bonus: Using keyboard keys displays the native osd* + +osd screenshot +
## Download @@ -15,24 +47,21 @@ Go to [Release](https://github.com/the0neyouseek/MonitorControl/releases/latest) ## How to help -Open [issues](./issues) if you have a question, an enhancement to suggest or a bug you've found. If you want you can fork the code yourself and submit a pull request to improve the app. +Open [issues](https://github.com/the0neyouseek/MonitorControl/issues) if you have a question, an enhancement to suggest or a bug you've found. If you want you can fork the code yourself and submit a pull request to improve the app. ## How to build ### Required -- XCode +- Xcode - [Cocoapods](https://cocoapods.org/) -- [SwiftLint](https://github.com/realm/SwiftLint) - -Download the [zip](https://github.com/the0neyouseek/MonitorControl/archive/master.zip) directly or clone the project somewhere with git +- [Swiftlint](https://github.com/realm/SwiftLint) +Clone the project ```sh -$ git clone https://github.com/the0neyouseek/MonitorControl.git +git clone https://github.com/the0neyouseek/MonitorControl.git --recurse-submodules ``` - Then download the dependencies with Cocoapods - ```sh $ pod install ``` @@ -42,13 +71,17 @@ You're all set ! Now open the `MonitorControl.xcworkspace` with Xcode ### Third party dependencies - [MediaKeyTap](https://github.com/the0neyouseek/MediaKeyTap) +- [MASPreferences](https://github.com/shpakovski/MASPreferences) +- [ddcctl](https://github.com/kfix/ddcctl) +- [AMCoreAudio](https://github.com/rnine/AMCoreAudio) ## Support - macOS Sierra (`10.12`) and up. -- Works with monitors comptaible with [@kfix/ddcctl](https://github.com/kfix/ddcctl) +- Works with monitors compatible with [@kfix/ddcctl](https://github.com/kfix/ddcctl) ## Thanks - [@bluejamesbond](https://github.com/bluejamesbond/) (Original developer) - [@Tyilo](https://github.com/Tyilo/) (Fork) - [@Bensge](https://github.com/Bensge/) - (Used some code from his project [NativeDisplayBrightness](https://github.com/Bensge/NativeDisplayBrightness)) -- [@nhurden](https://github.com/nhurden/) (For the original MediaKeyTap) \ No newline at end of file +- [@nhurden](https://github.com/nhurden/) (For the original MediaKeyTap) +- [@kfix](https://github.com/kfix/ddcctl) (For ddcctl)