♻️ Refactoring

- Key handling is now done in the app, close #3
- All code linted with SwiftLint, close #4
- Better handling of built-in screen

Signed-off-by: Guillaume Broder <iamnotheoneyouseek@gmail.com>
This commit is contained in:
Guillaume Broder 2018-01-06 16:26:03 +01:00
parent 455775617c
commit 68afdda765
No known key found for this signature in database
GPG key ID: 66FB02D063D9E08F
25 changed files with 501 additions and 280 deletions

45
.gitignore vendored
View file

@ -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

9
.swiftlint.yml Normal file
View file

@ -0,0 +1,9 @@
disabled_rules:
- line_length
- function_body_length
excluded:
- Pods
type_body_length:
- warning: 500
file_length:
- warning: 500

View file

@ -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 = "<group>"; };
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 = "<group>"; };
55359E331E2737EC002671BC /* DDC.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = DDC.c; sourceTree = "<group>"; };
55359E341E2737EC002671BC /* DDC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DDC.h; sourceTree = "<group>"; };
55359E351E2737EC002671BC /* ddcctl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ddcctl.m; sourceTree = "<group>"; };
@ -30,6 +35,7 @@
56754EB01D9A4016007BCDC5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
56754EB31D9A4016007BCDC5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
56754EB51D9A4016007BCDC5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F03A8DF11FFBAA6F0034DC27 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = "<group>"; };
F091C9B21F6EA6110096FD65 /* SliderHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderHandler.swift; sourceTree = "<group>"; };
F091C9B71F6EA79B0096FD65 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
F091C9B91F6EB43B0096FD65 /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = fr; path = fr.lproj/MainMenu.strings; sourceTree = "<group>"; };
@ -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 = "<group>";
};
@ -110,10 +118,20 @@
path = MonitorControl;
sourceTree = "<group>";
};
EFFC2F3E35BEC9ACFA754137 /* Pods */ = {
isa = PBXGroup;
children = (
42B61ABC1D7907131330228A /* Pods-MonitorControl.debug.xcconfig */,
31E16D90527EBD3F8A12BE0B /* Pods-MonitorControl.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
F091C9B41F6EA6180096FD65 /* Objects */ = {
isa = PBXGroup;
children = (
F091C9B21F6EA6110096FD65 /* SliderHandler.swift */,
F03A8DF11FFBAA6F0034DC27 /* Display.swift */,
);
path = Objects;
sourceTree = "<group>";
@ -122,6 +140,7 @@
isa = PBXGroup;
children = (
F0A987D81F77B404009B603D /* MonitorControl */,
398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -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;

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:MonitorControl.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View file

@ -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: "")
}
}

View file

@ -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"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>20</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>

View file

@ -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
}

View file

@ -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()
}

View file

@ -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: "")
}
}

10
Podfile Normal file
View file

@ -0,0 +1,10 @@
# Podfile
plateform :osx, '10.11'
target 'MonitorControl' do
use_frameworks!
pod 'MediaKeyTap', :git => 'https://github.com/the0neyouseek/MediaKeyTap.git'
end

12
Podfile.lock Normal file
View file

@ -0,0 +1,12 @@
PODS:
- MediaKeyTap (1.0.0)
DEPENDENCIES:
- MediaKeyTap
SPEC CHECKSUMS:
MediaKeyTap: 92764246d2ce8bf4446c2457e8b180e0b88926a1
PODFILE CHECKSUM: 11c0e07cdb4651a81ff3269f5d50664df18716d4
COCOAPODS: 1.3.1

View file

@ -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))
- [@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)