diff --git a/.gitignore b/.gitignore index f4e794d..4b28e73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,40 @@ -# Created by https://www.gitignore.io/api/xcode +# Created by https://www.gitignore.io/api/macos,xcode,cocoapods + +### CocoaPods ### +## CocoaPods GitIgnore Template + +# CocoaPods - Only use to conserve bandwidth / Save time on Pushing +# - Also handy if you have a large number of dependant pods +# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE +Pods/ + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk ### Xcode ### # Xcode @@ -25,4 +60,10 @@ xcuserdata/ *.moved-aside *.xccheckout *.xcscmblueprint -*.xcscheme + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..c328b1b --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,9 @@ +disabled_rules: +- line_length +- function_body_length +excluded: +- Pods +type_body_length: +- warning: 500 +file_length: +- warning: 500 diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj index d4d679e..102f51d 100644 --- a/MonitorControl.xcodeproj/project.pbxproj +++ b/MonitorControl.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 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 */; }; + 9A19D3B73485870616B6D4E0 /* Pods_MonitorControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */; }; + F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* Display.swift */; }; F091C9B31F6EA6110096FD65 /* SliderHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F091C9B21F6EA6110096FD65 /* SliderHandler.swift */; }; F091C9B81F6EA79B0096FD65 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F091C9B71F6EA79B0096FD65 /* Utils.swift */; }; F0A987E81F77B40E009B603D /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0A987D61F77B290009B603D /* OSD.framework */; }; @@ -19,6 +21,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 31E16D90527EBD3F8A12BE0B /* Pods-MonitorControl.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonitorControl.release.xcconfig"; path = "Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl.release.xcconfig"; sourceTree = ""; }; + 398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MonitorControl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 42B61ABC1D7907131330228A /* Pods-MonitorControl.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonitorControl.debug.xcconfig"; path = "Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl.debug.xcconfig"; sourceTree = ""; }; 55359E331E2737EC002671BC /* DDC.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = DDC.c; sourceTree = ""; }; 55359E341E2737EC002671BC /* DDC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DDC.h; sourceTree = ""; }; 55359E351E2737EC002671BC /* ddcctl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ddcctl.m; sourceTree = ""; }; @@ -30,6 +35,7 @@ 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 = ""; }; + F03A8DF11FFBAA6F0034DC27 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = ""; }; F091C9B21F6EA6110096FD65 /* SliderHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderHandler.swift; sourceTree = ""; }; F091C9B71F6EA79B0096FD65 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; F091C9B91F6EB43B0096FD65 /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = fr; path = fr.lproj/MainMenu.strings; sourceTree = ""; }; @@ -57,6 +63,7 @@ buildActionMask = 2147483647; files = ( F0A987E81F77B40E009B603D /* OSD.framework in Frameworks */, + 9A19D3B73485870616B6D4E0 /* Pods_MonitorControl.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,6 +91,7 @@ 55359E321E2737EC002671BC /* ddcctl */, 56754EAC1D9A4016007BCDC5 /* Products */, F0A987D71F77B404009B603D /* Frameworks */, + EFFC2F3E35BEC9ACFA754137 /* Pods */, ); sourceTree = ""; }; @@ -110,10 +118,20 @@ path = MonitorControl; sourceTree = ""; }; + EFFC2F3E35BEC9ACFA754137 /* Pods */ = { + isa = PBXGroup; + children = ( + 42B61ABC1D7907131330228A /* Pods-MonitorControl.debug.xcconfig */, + 31E16D90527EBD3F8A12BE0B /* Pods-MonitorControl.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; F091C9B41F6EA6180096FD65 /* Objects */ = { isa = PBXGroup; children = ( F091C9B21F6EA6110096FD65 /* SliderHandler.swift */, + F03A8DF11FFBAA6F0034DC27 /* Display.swift */, ); path = Objects; sourceTree = ""; @@ -122,6 +140,7 @@ isa = PBXGroup; children = ( F0A987D81F77B404009B603D /* MonitorControl */, + 398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */, ); name = Frameworks; sourceTree = ""; @@ -157,9 +176,13 @@ isa = PBXNativeTarget; buildConfigurationList = 56754EB81D9A4016007BCDC5 /* Build configuration list for PBXNativeTarget "MonitorControl" */; buildPhases = ( + C0EF20D28FC7408CBE89A686 /* [CP] Check Pods Manifest.lock */, + F03A8DF01FFB9D4C0034DC27 /* [Lint] Run SwiftLint */, 56754EA71D9A4016007BCDC5 /* Sources */, 56754EA81D9A4016007BCDC5 /* Frameworks */, 56754EA91D9A4016007BCDC5 /* Resources */, + 9DD5968596EFAF0E2EB56496 /* [CP] Embed Pods Frameworks */, + 3ACE91333FC1E781FB2E44E5 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -182,6 +205,7 @@ TargetAttributes = { 56754EAA1D9A4016007BCDC5 = { CreatedOnToolsVersion = 8.0; + DevelopmentTeam = KGY56RWR9A; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; @@ -220,11 +244,80 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 3ACE91333FC1E781FB2E44E5 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9DD5968596EFAF0E2EB56496 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/MediaKeyTap/MediaKeyTap.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MediaKeyTap.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C0EF20D28FC7408CBE89A686 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MonitorControl-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F03A8DF01FFB9D4C0034DC27 /* [Lint] Run SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[Lint] Run SwiftLint"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 56754EA71D9A4016007BCDC5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */, F091C9B31F6EA6110096FD65 /* SliderHandler.swift in Sources */, 56754EAF1D9A4016007BCDC5 /* AppDelegate.swift in Sources */, 55359E391E2737EC002671BC /* DDC.c in Sources */, @@ -393,13 +486,13 @@ }; 56754EB91D9A4016007BCDC5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 42B61ABC1D7907131330228A /* Pods-MonitorControl.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/**"; + DEVELOPMENT_TEAM = KGY56RWR9A; INFOPLIST_FILE = "$(SRCROOT)/MonitorControl/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl; @@ -412,13 +505,13 @@ }; 56754EBA1D9A4016007BCDC5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 31E16D90527EBD3F8A12BE0B /* Pods-MonitorControl.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/**"; + DEVELOPMENT_TEAM = KGY56RWR9A; INFOPLIST_FILE = "$(SRCROOT)/MonitorControl/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl; diff --git a/MonitorControl.xcworkspace/contents.xcworkspacedata b/MonitorControl.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..5412fae --- /dev/null +++ b/MonitorControl.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/MonitorControl/AppDelegate.swift b/MonitorControl/AppDelegate.swift index ccce703..74cebbe 100644 --- a/MonitorControl/AppDelegate.swift +++ b/MonitorControl/AppDelegate.swift @@ -9,227 +9,130 @@ import Cocoa import Foundation - -struct Display { - var id: CGDirectDisplayID - var name: String - var serial: String -} +import MediaKeyTap var app: AppDelegate! = nil let prefs = UserDefaults.standard @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate { - +class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate { + @IBOutlet weak var statusMenu: NSMenu! @IBOutlet weak var window: NSWindow! - + let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) var monitorItems: [NSMenuItem] = [] var displays: [Display] = [] - var sliderHandlers: [SliderHandler] = [] + var sliderHandlers: [SliderHandler] = [] var defaultDisplay: Display! = nil var defaultBrightnessSlider: NSSlider! = nil var defaultVolumeSlider: NSSlider! = nil - let step = 100/16; + let step = 100/16 - @IBAction func quitClicked(_ sender: AnyObject) { - NSApplication.shared.terminate(self) - } + var mediaKeyTap: MediaKeyTap? func applicationDidFinishLaunching(_ aNotification: Notification) { app = self + mediaKeyTap = MediaKeyTap.init(delegate: self, forKeys: [.brightnessUp, .brightnessDown, .mute, .volumeUp, .volumeDown], observeBuiltIn: false) statusItem.image = NSImage.init(named: NSImage.Name(rawValue: "status")) statusItem.menu = statusMenu - acquirePrivileges() + Utils.acquirePrivileges() - CGDisplayRegisterReconfigurationCallback({_,_,_ in app.updateDisplays()}, nil) + CGDisplayRegisterReconfigurationCallback({_, _, _ in app.updateDisplays()}, nil) updateDisplays() - NSEvent.addGlobalMonitorForEvents( - matching: NSEvent.EventTypeMask.keyDown, handler: {(event: NSEvent) in - if self.defaultDisplay == nil { - return - } - - // Keyboard shortcut only for main screen - let currentDisplayId = NSScreen.main?.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as! CGDirectDisplayID - if (self.defaultDisplay.id != currentDisplayId) { - return - } - - // Brightness -> Shift + Control + Alt + Command + (Up/Down) - // Volume -> Shift + Control + Alt + Command + (Left/Right) - // Mute -> Minus - - // Capture keys - let modifiers = NSEvent.ModifierFlags.init(rawValue: NSEvent.ModifierFlags.shift.rawValue | NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.control.rawValue | NSEvent.ModifierFlags.option.rawValue) - let flags = event.modifierFlags.intersection(modifiers) - - // Only do something if all modifiers are active - if !flags.contains(NSEvent.ModifierFlags.shift) || !flags.contains(NSEvent.ModifierFlags.command) || !flags.contains(NSEvent.ModifierFlags.control) || !flags.contains(NSEvent.ModifierFlags.option) { - return - } - - var brightnessRel = 0 - var volumeRel = 0 - var rel = 0 - - // Down key - if event.keyCode == Utils.key.keyDownArrow.rawValue { - brightnessRel = -self.step - // Up key - } else if event.keyCode == Utils.key.keyUpArrow.rawValue { - brightnessRel = +self.step - // Left key - } else if event.keyCode == Utils.key.keyLeftArrow.rawValue { - volumeRel = -self.step - // Right key - } else if event.keyCode == Utils.key.keyRightArrow.rawValue { - volumeRel = +self.step - // M key - } else if event.keyCode == Utils.key.keyMute.rawValue { - volumeRel = -100 - } else { - return - } - - var command = Int32() - var slider: NSSlider! = nil - if brightnessRel == 0 { - command = AUDIO_SPEAKER_VOLUME - slider = self.defaultVolumeSlider - rel = volumeRel - } else if volumeRel == 0 { - command = BRIGHTNESS - slider = self.defaultBrightnessSlider - rel = brightnessRel - } else { - return - } - - let k = "\(command)-\(self.defaultDisplay.serial)" - let value = max(0, min(100, prefs.integer(forKey: k) + rel)) - - prefs.setValue(value, forKey: k) - prefs.synchronize() - slider.intValue = Int32(value) - - Utils.ddcctl(monitor: self.defaultDisplay.id, command: command, value: value) - - // OSD - let manager : OSDManager = OSDManager.sharedManager() as! OSDManager - var osdImage : Int = 1 // Brightness Image - if brightnessRel == 0 { - osdImage = 3 // Speaker image - if value == 0 { - osdImage = 4 // Mute speaker - } - } - manager.showImage(Int64(osdImage), onDisplayID: self.defaultDisplay.id, priority: 0x1f4, msecUntilFade: 2000, filledChiclets: UInt32(value/self.step), totalChiclets: UInt32(100/self.step), locked: false) - }) + mediaKeyTap?.start() } - func addSliderItem(menu: NSMenu, isDefaultDisplay: Bool, display: Display, command: Int32, title: String) -> NSSlider { - let item = NSMenuItem() + func applicationWillTerminate(_ aNotification: Notification) { + } - let view = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 40)) + @IBAction func quitClicked(_ sender: AnyObject) { + NSApplication.shared.terminate(self) + } - let label = Utils.makeLabel(text: title, frame: NSRect(x: 20, y: 19, width: 130, height: 20)) + // MARK: - Menu - let handler = SliderHandler(display: display, command: command) - sliderHandlers.append(handler) + func clearDisplays() { + defaultDisplay = nil + defaultBrightnessSlider = nil + defaultVolumeSlider = nil - let slider = NSSlider(frame: NSRect(x: 20, y: 0, width: 200, height: 19)) - slider.target = handler - slider.minValue = 0 - slider.maxValue = 100 - slider.integerValue = prefs.integer(forKey: "\(command)-\(display.serial)") - slider.action = #selector(SliderHandler.valueChanged) + for monitor in monitorItems { + statusMenu.removeItem(monitor) + } - view.addSubview(label) - view.addSubview(slider) - - item.view = view - - menu.addItem(item) - menu.addItem(NSMenuItem.separator()) - - return slider - } + monitorItems = [] + displays = [] + sliderHandlers = [] + } func updateDisplays() { - defaultDisplay = nil - defaultBrightnessSlider = nil - defaultVolumeSlider = nil - - for m in monitorItems { - statusMenu.removeItem(m) - } - - monitorItems = [] - displays = [] - sliderHandlers = [] - + clearDisplays() sleep(1) - for s in NSScreen.screens { - let id = s.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as! CGDirectDisplayID - if CGDisplayIsBuiltin(id) != 0 { - continue - } + for screen in NSScreen.screens { + if let id = screen.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as? CGDirectDisplayID { + // Is Built In Screen (e.g. MBP/iMac Screen) + if CGDisplayIsBuiltin(id) != 0 { + continue + } - var edid = EDID() - if !EDIDTest(id, &edid) { - continue - } + // Does screen support EDID ? + var edid = EDID() + if !EDIDTest(id, &edid) { + continue + } - let name = getDisplayName(edid) - let serial = getDisplaySerial(edid) + let name = Utils.getDisplayName(forEdid: edid) + let serial = Utils.getDisplaySerial(forEdid: edid) - let isDefaultDisplay = defaultDisplay == nil + let display = Display(identifier: id, name: name, serial: serial, isBuiltIn: false) + displays.append(display) - let d = Display(id: id, name: name, serial: serial) - displays.append(d) + let monitorSubMenu = NSMenu() + let brightnessSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, + forDisplay: display, + command: BRIGHTNESS, + title: NSLocalizedString("Brightness", comment: "Shown in menu")) + let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu, + forDisplay: display, + command: AUDIO_SPEAKER_VOLUME, + title: NSLocalizedString("Volume", comment: "Shown in menu")) + sliderHandlers.append(brightnessSliderHandler) + sliderHandlers.append(volumeSliderHandler) - let monitorMenuItem = NSMenuItem() - let monitorSubMenu = NSMenu() + let isDefaultDisplay = defaultDisplay == nil + let defaultMonitorSelectButtom = NSButton(frame: NSRect(x: 25, y: 0, width: 200, height: 25)) + defaultMonitorSelectButtom.title = isDefaultDisplay ? NSLocalizedString("Default", comment: "Shown in menu") : NSLocalizedString("Set as default", comment: "Shown in menu") + defaultMonitorSelectButtom.bezelStyle = NSButton.BezelStyle.rounded + defaultMonitorSelectButtom.isEnabled = !isDefaultDisplay - let brightnessSlider = addSliderItem(menu: monitorSubMenu, isDefaultDisplay: isDefaultDisplay, display: d, command: BRIGHTNESS, title: NSLocalizedString("Brightness", comment: "Sown in menu")) - let _ = addSliderItem(menu: monitorSubMenu, isDefaultDisplay: isDefaultDisplay, display: d, command: CONTRAST, title: NSLocalizedString("Contrast", comment: "Shown in menu")) - let volumeSlider = addSliderItem(menu: monitorSubMenu, isDefaultDisplay: isDefaultDisplay, display: d, command: AUDIO_SPEAKER_VOLUME, title: NSLocalizedString("Volume", comment: "Shown in menu")) + let defaultMonitorView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 25)) + defaultMonitorView.addSubview(defaultMonitorSelectButtom) - let defaultMonitorItem = NSMenuItem() - let defaultMonitorView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 25)) + let defaultMonitorItem = NSMenuItem() + defaultMonitorItem.view = defaultMonitorView + monitorSubMenu.addItem(defaultMonitorItem) - let defaultMonitorSelectButtom = NSButton(frame: NSRect(x: 25, y: 0, width: 200, height: 25)) - defaultMonitorSelectButtom.title = isDefaultDisplay ? NSLocalizedString("Default", comment: "Shown in menu") : NSLocalizedString("Set as default", comment: "Shown in menu") - defaultMonitorSelectButtom.bezelStyle = NSButton.BezelStyle.rounded - defaultMonitorSelectButtom.isEnabled = !isDefaultDisplay + let monitorMenuItem = NSMenuItem() + monitorMenuItem.title = "\(name)" + monitorMenuItem.submenu = monitorSubMenu - defaultMonitorView.addSubview(defaultMonitorSelectButtom) + monitorItems.append(monitorMenuItem) + statusMenu.insertItem(monitorMenuItem, at: displays.count - 1) - defaultMonitorItem.view = defaultMonitorView - - monitorSubMenu.addItem(defaultMonitorItem) - - monitorMenuItem.title = "\(name)" - monitorMenuItem.submenu = monitorSubMenu - - monitorItems.append(monitorMenuItem) - statusMenu.insertItem(monitorMenuItem, at: displays.count - 1) - - if isDefaultDisplay { - defaultDisplay = d - defaultBrightnessSlider = brightnessSlider - defaultVolumeSlider = volumeSlider - } + if isDefaultDisplay { + defaultDisplay = display + defaultBrightnessSlider = brightnessSliderHandler.slider + defaultVolumeSlider = volumeSliderHandler.slider + } + } } if defaultDisplay == nil { @@ -241,51 +144,64 @@ class AppDelegate: NSObject, NSApplicationDelegate { statusMenu.insertItem(item, at: 0) } } - - func acquirePrivileges() { - let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true] - let accessibilityEnabled = AXIsProcessTrustedWithOptions(options) - - if !accessibilityEnabled { - print(NSLocalizedString("You need to enable the keylogger in the System Prefrences for the keyboard shortcuts to work", comment: "")) - } - - return - } - - func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application - } - func edidString(_ d: descriptor) -> String { - var s = "" - for (_, b) in Mirror(reflecting: d.text.data).children { - let b = b as! Int8 - let c = Character(UnicodeScalar(UInt8(bitPattern: b))) - if c == "\0" || c == "\n" { - break - } - s.append(c) - } - return s - } + // MARK: - Media Key Tap delegate - func getDescriptorString(_ edid: EDID, _ type: UInt8) -> String? { - for (_, d) in Mirror(reflecting: edid.descriptors).children { - let d = d as! descriptor - if d.text.type == UInt8(type) { - return edidString(d) - } - } + func handle(mediaKey: MediaKey, event: KeyEvent) { - return nil - } + var command = BRIGHTNESS + var rel = 0 + var slider = self.defaultBrightnessSlider - func getDisplayName(_ edid: EDID) -> String { - return getDescriptorString(edid, 0xFC) ?? NSLocalizedString("Display", comment: "") - } + switch mediaKey { + case .brightnessUp: + rel = +self.step + case .brightnessDown: + rel = -self.step + case .mute: + rel = -100 + command = AUDIO_SPEAKER_VOLUME + slider = self.defaultVolumeSlider + case .volumeUp: + rel = +self.step + command = AUDIO_SPEAKER_VOLUME + slider = self.defaultVolumeSlider + case .volumeDown: + rel = -self.step + command = AUDIO_SPEAKER_VOLUME + slider = self.defaultVolumeSlider + default: + return + } + + let k = "\(command)-\(self.defaultDisplay.serial)" + let value = max(0, min(100, prefs.integer(forKey: k) + rel)) + prefs.setValue(value, forKey: k) + prefs.synchronize() + + if let slider = slider { + slider.intValue = Int32(value) + } + + Utils.ddcctl(monitor: self.defaultDisplay.identifier, command: command, value: value) + + // OSD + if let manager = OSDManager.sharedManager() as? OSDManager { + var osdImage: Int64 = 1 // Brightness Image + if command == AUDIO_SPEAKER_VOLUME { + osdImage = 3 // Speaker image + if value == 0 { + osdImage = 4 // Mute speaker + } + } + manager.showImage(osdImage, + onDisplayID: self.defaultDisplay.identifier, + priority: 0x1f4, + msecUntilFade: 2000, + filledChiclets: UInt32(value/self.step), + totalChiclets: UInt32(100/self.step), + locked: false) + } + } - func getDisplaySerial(_ edid: EDID) -> String { - return getDescriptorString(edid, 0xFF) ?? NSLocalizedString("Unknown", comment: "") - } } diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Contents.json b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Contents.json index 2db2b1c..d296f15 100644 --- a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,53 +1,63 @@ { "images" : [ { - "idiom" : "mac", "size" : "16x16", + "idiom" : "mac", + "filename" : "Icon-16.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "16x16", + "idiom" : "mac", + "filename" : "Icon-33.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "32x32", + "idiom" : "mac", + "filename" : "Icon-32.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "32x32", + "idiom" : "mac", + "filename" : "Icon-64.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "128x128", + "idiom" : "mac", + "filename" : "Icon-128.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "128x128", + "idiom" : "mac", + "filename" : "Icon-257.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "256x256", + "idiom" : "mac", + "filename" : "Icon-256.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "256x256", + "idiom" : "mac", + "filename" : "Icon-513.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "512x512", + "idiom" : "mac", + "filename" : "Icon-512.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "512x512", + "idiom" : "mac", + "filename" : "Icon-1024.png", "scale" : "2x" } ], diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-1024.png new file mode 100644 index 0000000..9bb9765 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-1024.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-128.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-128.png new file mode 100644 index 0000000..7dffaf7 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-128.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-16.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-16.png new file mode 100644 index 0000000..038f848 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-16.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-256.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-256.png new file mode 100644 index 0000000..f517ca4 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-256.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-257.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-257.png new file mode 100644 index 0000000..f517ca4 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-257.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-32.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-32.png new file mode 100644 index 0000000..ff6ce19 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-32.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-33.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-33.png new file mode 100644 index 0000000..ff6ce19 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-33.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-512.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-512.png new file mode 100644 index 0000000..5b658f6 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-512.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-513.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-513.png new file mode 100644 index 0000000..5b658f6 Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-513.png differ diff --git a/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-64.png b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-64.png new file mode 100644 index 0000000..81f7ffa Binary files /dev/null and b/MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-64.png differ diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 2fe6225..3edcac1 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.1 CFBundleVersion - 1 + 20 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Objects/Display.swift b/MonitorControl/Objects/Display.swift new file mode 100644 index 0000000..7d41321 --- /dev/null +++ b/MonitorControl/Objects/Display.swift @@ -0,0 +1,17 @@ +// +// Display.swift +// MonitorControl +// +// Created by Guillaume BRODER on 02/01/2018. +// Copyright © 2018 Mathew Kurian. All rights reserved. +// + +import Cocoa + +/// A display +struct Display { + var identifier: CGDirectDisplayID + var name: String + var serial: String + var isBuiltIn: Bool = false +} diff --git a/MonitorControl/Objects/SliderHandler.swift b/MonitorControl/Objects/SliderHandler.swift index c470e8e..119ab77 100644 --- a/MonitorControl/Objects/SliderHandler.swift +++ b/MonitorControl/Objects/SliderHandler.swift @@ -8,29 +8,31 @@ import Cocoa -class SliderHandler: NSObject { - var display : Display - var command : Int32 = 0 - +/// Handle the slider +class SliderHandler { + var slider: NSSlider? + var display: Display + var command: Int32 = 0 + public init(display: Display, command: Int32) { self.display = display self.command = command } - + @objc func valueChanged(slider: NSSlider) { let snapInterval = 25 let snapThreshold = 3 - + var value = slider.integerValue - + let closest = (value + snapInterval / 2) / snapInterval * snapInterval if abs(closest - value) <= snapThreshold { value = closest slider.integerValue = value } - - Utils.ddcctl(monitor: display.id, command: command, value: value) - + + Utils.ddcctl(monitor: display.identifier, command: command, value: value) + prefs.setValue(value, forKey: "\(command)-\(display.serial)") prefs.synchronize() } diff --git a/MonitorControl/Utils.swift b/MonitorControl/Utils.swift index 15ad8db..2827ff8 100644 --- a/MonitorControl/Utils.swift +++ b/MonitorControl/Utils.swift @@ -9,7 +9,9 @@ import Cocoa class Utils: NSObject { - + + // MARK: - DDCCTL + /// Send command to ddcctl /// /// - Parameters: @@ -21,7 +23,9 @@ class Utils: NSObject { DDCWrite(monitor, &wrcmd) print(value) } - + + // MARK: - Menu + /// Create a label /// /// - Parameters: @@ -38,18 +42,109 @@ class Utils: NSObject { return label } - - /// Enum for hardware independent keyCode + /// Create a slider and add it to the menu /// - /// - keyLeftArrow: keyCode for the left arrow - /// - keyRightArrow: keyCode for the right arrow - /// - keyDownArrow: keyCode for the down arrow - /// - keyUpArrow: keyCode for the up arrow - enum key : Int { - case keyLeftArrow = 123 - case keyRightArrow = 124 - case keyDownArrow = 125 - case keyUpArrow = 126 - case keyMute = 24 + /// - 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: Display, command: Int32, title: String) -> SliderHandler { + let item = NSMenuItem() + let view = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 40)) + let label = Utils.makeLabel(text: title, frame: NSRect(x: 20, y: 19, width: 130, height: 20)) + let handler = SliderHandler(display: display, command: command) + let slider = NSSlider(frame: NSRect(x: 20, y: 0, width: 200, height: 19)) + slider.target = handler + slider.minValue = 0 + slider.maxValue = 100 + slider.integerValue = prefs.integer(forKey: "\(command)-\(display.serial)") + slider.action = #selector(SliderHandler.valueChanged) + handler.slider = slider + + view.addSubview(label) + view.addSubview(slider) + + item.view = view + + menu.addItem(item) + menu.addItem(NSMenuItem.separator()) + + return handler } + + // MARK: - Utilities + + /// Acquire Privileges (Necessary to listen to keyboard event globally) + static func acquirePrivileges() { + let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true] + let accessibilityEnabled = AXIsProcessTrustedWithOptions(options) + + if !accessibilityEnabled { + let alert = NSAlert() + alert.addButton(withTitle: NSLocalizedString("Ok", comment: "Shown in the alert dialog")) + alert.messageText = NSLocalizedString("Shortcuts not available", comment: "Shown in the alert dialog") + alert.informativeText = NSLocalizedString("You need to enable MonitorControl in System Preferences > Security and Privacy > Accessibility for the keyboard shortcuts to work", comment: "Shown in the alert dialog") + alert.alertStyle = .warning + alert.runModal() + } + + return + } + + // MARK: - Display Infos + + /// Get the descriptor text + /// + /// - Parameter descriptor: the descriptor + /// - Returns: a string + static func getEdidString(_ descriptor: descriptor) -> String { + var result = "" + for (_, bitChar) in Mirror(reflecting: descriptor.text.data).children { + if let bitChar = bitChar as? Int8 { + let char = Character(UnicodeScalar(UInt8(bitPattern: bitChar))) + if char == "\0" || char == "\n" { + break + } + result.append(char) + } + } + return result + } + + /// Get the descriptors of a display from the Edid + /// + /// - Parameters: + /// - edid: the EDID of a display + /// - type: the type of descriptor + /// - Returns: a string if type of descriptor is found + static func getDescriptorString(_ edid: EDID, _ type: UInt8) -> String? { + for (_, descriptor) in Mirror(reflecting: edid.descriptors).children { + if let descriptor = descriptor as? descriptor { + if descriptor.text.type == UInt8(type) { + return getEdidString(descriptor) + } + } + } + + return nil + } + + /// Get the name of a display + /// + /// - Parameter edid: the EDID of a display + /// - Returns: a string + static func getDisplayName(forEdid edid: EDID) -> String { + return getDescriptorString(edid, 0xFC) ?? NSLocalizedString("Display", comment: "") + } + + /// Get the serial of a display + /// + /// - Parameter edid: the EDID of a display + /// - Returns: a string + static func getDisplaySerial(forEdid edid: EDID) -> String { + return getDescriptorString(edid, 0xFF) ?? NSLocalizedString("Unknown", comment: "") + } + } diff --git a/MonitorControl/en.lproj/Localizable.strings b/MonitorControl/en.lproj/Localizable.strings index 02e8298..50fff37 100644 Binary files a/MonitorControl/en.lproj/Localizable.strings and b/MonitorControl/en.lproj/Localizable.strings differ diff --git a/MonitorControl/fr.lproj/Localizable.strings b/MonitorControl/fr.lproj/Localizable.strings index 61c7556..f7c0f73 100644 Binary files a/MonitorControl/fr.lproj/Localizable.strings and b/MonitorControl/fr.lproj/Localizable.strings differ diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..f9f0e86 --- /dev/null +++ b/Podfile @@ -0,0 +1,10 @@ +# Podfile + +plateform :osx, '10.11' + +target 'MonitorControl' do + use_frameworks! + +pod 'MediaKeyTap', :git => 'https://github.com/the0neyouseek/MediaKeyTap.git' + +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..162f63b --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,12 @@ +PODS: + - MediaKeyTap (1.0.0) + +DEPENDENCIES: + - MediaKeyTap + +SPEC CHECKSUMS: + MediaKeyTap: 92764246d2ce8bf4446c2457e8b180e0b88926a1 + +PODFILE CHECKSUM: 11c0e07cdb4651a81ff3269f5d50664df18716d4 + +COCOAPODS: 1.3.1 diff --git a/README.md b/README.md index f6c88f3..c1a2f33 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,47 @@ # MonitorControl -Control your external monitor brightness, contrast or volume directly from a menulet or with keyboard shortcuts : - -- Brightness: `⇧` + `⌃` + `⌥` + `⌘` + `↑/↓` (Shift + Control + Alt + Command + Up/Down arrows) -- Volume: `⇧` + `⌃` + `⌥` + `⌘` + `←/→` (Shift + Control + Alt + Command + Left/Right arrows) -- Mute: `⇧` + `⌃` + `⌥` + `⌘` + `-` (Shift + Control + Alt + Command + Minus) - -(Ps. The keyboard shortcut only work for the default screen) +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) + + ## Download Go to [Release](https://github.com/the0neyouseek/MonitorControl/releases/latest) and download the latest `.dmg` -## Brightness/Volume default key -You can use [Karabiner Elements](https://github.com/tekezo/Karabiner-Elements/) to use the default mac key (`F1`, `F2` for brightness and `F10`, `F11`, `F12` for volume) with this set of custom rules : -[Karabiner rules for MonitorControl](./.github/rules.json) +## How to help -Copy and paste this url in your browser to install them directly : +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. -``` -karabiner://karabiner/assets/complex_modifications/import?url=https%3A%2F%2Fraw.githubusercontent.com%2Fthe0neyouseek%2FMonitorControl%2Fmaster%2F.github%2Frules.json +## How to build + +### Required + +- 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 + +```sh +$ git clone https://github.com/the0neyouseek/MonitorControl.git ``` ---- +Then download the dependencies with Cocoapods -Bonus: Using keyboard shortcuts display the native osd : +```sh +$ pod install +``` -![MonitorControl OSD](./.github/osd.png) +You're all set ! Now open the `MonitorControl.xcworkspace` with Xcode -## TODO +### Third party dependencies -- [ ] Hande multiple screen for keyboard shortcut (Possibly the choice to have all screen brightness/volume increase/decrease at the same time or separatly) -- [ ] Skip Karabiner use for keyboard shortcut -- [ ] Option to start app at login -- [ ] Add [SwiftLint](https://github.com/realm/SwiftLint) -- [ ] Change App Icon +- [MediaKeyTap](https://github.com/the0neyouseek/MediaKeyTap) ## Support - macOS Sierra (`10.12`) and up. @@ -45,4 +50,5 @@ Bonus: Using keyboard shortcuts display the native osd : ## 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)) \ No newline at end of file +- [@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