Project upgrade, stability improvements, new features (#59)

PR #59 with enhancements and fixes (Changes done by @JoniVR):

- Code migration to **Swift 4.2**
- **Fixed** the **MonitorControl scheme** which was gone for me
- **Updated the screenshots and readme**
- **Added a feature that detects the default sound output device and only intercepts when an external display is set as default output**, so Airpods, internal macbook volume and other non-monitor devices will be ignored (#48, #15)
- **Fixed app unresponsiveness and high CPU on launch** (might be related to #50, #37, please do keep in mind that on initial launch there will still be a little unresponsiveness because the underlying framework - called [ddcctl](https://github.com/kfix/ddcctl) - attempts to read your volume settings 10 times)
- Added a **version and build number** to the main preferences panel:
![menugeneral](https://user-images.githubusercontent.com/7591717/51052474-0c4c7500-15d7-11e9-883d-804051577bc7.png)
- Added a build script that increases the build number on each run. Version number still needs to be set manually.
- refactored some parts of the code to make it a bit more readable.
This commit is contained in:
Joni Van Roost 2019-02-04 20:50:09 +01:00 committed by Guillaume B
parent 2f4107c5fa
commit 830878ee1d
19 changed files with 531 additions and 317 deletions

BIN
.github/menudisplay.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
.github/menugeneral.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
.github/menukeys.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
.github/menulet-outdated.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
.github/menulet.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Before After
Before After

BIN
.github/osd.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
.github/osd.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -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 = "<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>"; };
6C778D5921E91060000A4D5F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
6C778D5E21E910A2000A4D5F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = "<group>"; };
6C778D5F21E910A6000A4D5F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = "<group>"; };
F03A8DF11FFBAA6F0034DC27 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = "<group>"; };
F0445D3720023E710025AE82 /* MainPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPrefsViewController.swift; sourceTree = "<group>"; };
F0445D3B200254FA0025AE82 /* KeysPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysPrefsViewController.swift; sourceTree = "<group>"; };
F0445D3F200259C10025AE82 /* DisplayPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPrefsViewController.swift; sourceTree = "<group>"; };
F0445D42200282E60025AE82 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
F0445D45200282EB0025AE82 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = "<group>"; };
F0445D47200282F80025AE82 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = "<group>"; };
F0445D49200285690025AE82 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = "<group>"; };
F0445D4B2002856C0025AE82 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/MainMenu.strings; sourceTree = "<group>"; };
F0445D4C200294AB0025AE82 /* ButtonCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCellView.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 = "<group>";
@ -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;
};

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56754EAA1D9A4016007BCDC5"
BuildableName = "MonitorControl.app"
BlueprintName = "MonitorControl"
ReferencedContainer = "container:MonitorControl.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56754EAA1D9A4016007BCDC5"
BuildableName = "MonitorControl.app"
BlueprintName = "MonitorControl"
ReferencedContainer = "container:MonitorControl.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56754EAA1D9A4016007BCDC5"
BuildableName = "MonitorControl.app"
BlueprintName = "MonitorControl"
ReferencedContainer = "container:MonitorControl.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56754EAA1D9A4016007BCDC5"
BuildableName = "MonitorControl.app"
BlueprintName = "MonitorControl"
ReferencedContainer = "container:MonitorControl.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -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 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)
}
}
@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)
/// 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)
prefs.set(false, forKey: Utils.PrefKeys.startAtLogin.rawValue)
prefs.set(false, forKey: Utils.PrefKeys.startAtLogin.rawValue)
prefs.set(false, forKey: Utils.PrefKeys.showContrast.rawValue)
prefs.set(false, forKey: Utils.PrefKeys.lowerContrast.rawValue)
}
}
prefs.set(false, forKey: Utils.PrefKeys.showContrast.rawValue)
prefs.set(false, forKey: Utils.PrefKeys.lowerContrast.rawValue)
}
}
// MARK: - Menu
func clearDisplays() {
if statusMenu.items.count > 2 {
var items: [NSMenuItem] = []
for i in 0..<statusMenu.items.count - 2 {
items.append(statusMenu.items[i])
}
// MARK: - Menu
for item in items {
statusMenu.removeItem(item)
}
}
func clearDisplays() {
if statusMenu.items.count > 2 {
var items: [NSMenuItem] = []
for i in 0..<statusMenu.items.count - 2 {
items.append(statusMenu.items[i])
}
for item in items {
statusMenu.removeItem(item)
}
}
monitorItems = []
displays = []
}
monitorItems = []
displays = []
}
func updateDisplays() {
clearDisplays()
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
}
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
}
// Does screen support EDID ?
var edid = EDID()
if !EDIDTest(id, &edid) {
return false
}
return true
}
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 == 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 {
/// 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)
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 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
}
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)
display.volumeSliderHandler = volumeSliderHandler
display.brightnessSliderHandler = brightnessSliderHandler
displays.append(display)
let monitorMenuItem = NSMenuItem()
monitorMenuItem.title = "\(name)"
if asSubMenu {
monitorMenuItem.submenu = monitorSubMenu
}
let monitorMenuItem = NSMenuItem()
monitorMenuItem.title = "\(name)"
if asSubMenu {
monitorMenuItem.submenu = monitorSubMenu
}
monitorItems.append(monitorMenuItem)
statusMenu.insertItem(monitorMenuItem, at: 0)
}
}
}
monitorItems.append(monitorMenuItem)
statusMenu.insertItem(monitorMenuItem, at: 0)
}
}
}
// MARK: - Media Key Tap delegate
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"))
}
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()
}
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()
}
}

View file

@ -1,85 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Main Prefs View Controller-->
<scene sceneID="Bdy-Ug-m8G">
<objects>
<viewController storyboardIdentifier="MainPrefsVC" id="HNb-aq-vnV" customClass="MainPrefsViewController" customModule="MonitorControl" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="iuQ-z6-BGz">
<rect key="frame" x="0.0" y="0.0" width="400" height="161"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8KQ-j9-ddv">
<rect key="frame" x="18" y="112" width="92" height="29"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="General" id="ocE-Cc-2bi">
<font key="font" metaFont="systemBold" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dZG-M6-fsQ">
<rect key="frame" x="18" y="86" width="199" height="18"/>
<buttonCell key="cell" type="check" title="Start MonitorControl at Login" bezelStyle="regularSquare" imagePosition="left" inset="2" id="UTh-SV-vAQ">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="startAtLoginClicked:" target="HNb-aq-vnV" id="OrA-9Y-N8S"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7YI-Uo-3fM">
<rect key="frame" x="18" y="52" width="177" height="18"/>
<buttonCell key="cell" type="check" title="Show a slider for contrast" bezelStyle="regularSquare" imagePosition="left" inset="2" id="8cS-Fg-fKy">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="showContrastSliderClicked:" target="HNb-aq-vnV" id="9BB-1N-fjo"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rYO-48-phB">
<rect key="frame" x="18" y="18" width="215" height="18"/>
<buttonCell key="cell" type="check" title="Lower Contrast after Brightness" bezelStyle="regularSquare" imagePosition="left" inset="2" id="JDl-l4-s8k">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="lowerContrastClicked:" target="HNb-aq-vnV" id="hki-Wc-jOF"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="dZG-M6-fsQ" firstAttribute="leading" secondItem="iuQ-z6-BGz" secondAttribute="leading" constant="20" id="8QT-EO-uBJ"/>
<constraint firstItem="rYO-48-phB" firstAttribute="leading" secondItem="iuQ-z6-BGz" secondAttribute="leading" constant="20" id="Cyy-k9-19a"/>
<constraint firstItem="8KQ-j9-ddv" firstAttribute="top" secondItem="iuQ-z6-BGz" secondAttribute="top" constant="20" id="Fwh-JQ-1Cx"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="7YI-Uo-3fM" secondAttribute="trailing" constant="20" id="Jtd-pc-orO"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="rYO-48-phB" secondAttribute="trailing" constant="20" id="XEL-Kb-iUl"/>
<constraint firstAttribute="bottom" secondItem="rYO-48-phB" secondAttribute="bottom" constant="20" id="YZb-sI-14r"/>
<constraint firstItem="7YI-Uo-3fM" firstAttribute="leading" secondItem="iuQ-z6-BGz" secondAttribute="leading" constant="20" id="YhE-2k-Edh"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="dZG-M6-fsQ" secondAttribute="trailing" constant="20" id="c5h-vI-6Dg"/>
<constraint firstItem="7YI-Uo-3fM" firstAttribute="top" secondItem="dZG-M6-fsQ" secondAttribute="bottom" constant="20" id="cdD-7n-hpx"/>
<constraint firstItem="dZG-M6-fsQ" firstAttribute="top" secondItem="8KQ-j9-ddv" secondAttribute="bottom" constant="10" id="dPJ-ev-Y3O"/>
<constraint firstItem="rYO-48-phB" firstAttribute="top" secondItem="7YI-Uo-3fM" secondAttribute="bottom" constant="20" id="fF5-Zz-fCl"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="dZG-M6-fsQ" secondAttribute="bottom" constant="20" id="gVa-4Q-fPP"/>
<constraint firstItem="8KQ-j9-ddv" firstAttribute="leading" secondItem="iuQ-z6-BGz" secondAttribute="leading" constant="20" id="ii0-uV-Ylg"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="8KQ-j9-ddv" secondAttribute="trailing" constant="20" id="qVn-hz-dRV"/>
</constraints>
</view>
<connections>
<outlet property="lowerContrast" destination="rYO-48-phB" id="hSm-0P-Uur"/>
<outlet property="showContrastSlider" destination="7YI-Uo-3fM" id="bPT-Jq-Lfc"/>
<outlet property="startAtLogin" destination="dZG-M6-fsQ" id="Rwg-dp-vIj"/>
</connections>
</viewController>
<customObject id="ALN-AB-CU5" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="156" y="227"/>
</scene>
<!--Keys Prefs View Controller-->
<scene sceneID="DmR-ia-4qL">
<objects>
@ -89,7 +15,7 @@
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fUf-UM-yW3">
<rect key="frame" x="85" y="17" width="196" height="26"/>
<rect key="frame" x="85" y="17" width="197" height="25"/>
<popUpButtonCell key="cell" type="push" title="Both Brightness &amp; Volume" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Vr4-xb-B4o" id="DkZ-as-YDS">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -177,7 +103,7 @@
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="" width="49" minWidth="40" maxWidth="1000" id="8U8-ec-Zbv">
<tableColumn width="49" minWidth="40" maxWidth="1000" id="8U8-ec-Zbv">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Enabled">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@ -208,7 +134,7 @@
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="" width="186" minWidth="40" maxWidth="1000" id="CHc-s5-4MN">
<tableColumn width="186" minWidth="40" maxWidth="1000" id="CHc-s5-4MN">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Display Name">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@ -241,7 +167,7 @@
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="" width="114" minWidth="10" maxWidth="3.4028234663852886e+38" id="dgp-q7-cBK">
<tableColumn width="114" minWidth="10" maxWidth="3.4028234663852886e+38" id="dgp-q7-cBK">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Display Id">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@ -282,11 +208,11 @@
</tableView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="UHx-W7-xSv">
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="UHx-W7-xSv">
<rect key="frame" x="-100" y="-100" width="358" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="HBR-36-UaL">
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="HBR-36-UaL">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
@ -328,5 +254,90 @@
</objects>
<point key="canvasLocation" x="1116" y="234"/>
</scene>
<!--Main Prefs View Controller-->
<scene sceneID="zAg-r8-WQ5">
<objects>
<viewController storyboardIdentifier="MainPrefsVC" id="BGD-tY-Myx" customClass="MainPrefsViewController" customModule="MonitorControl" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="COE-Oc-gZs">
<rect key="frame" x="0.0" y="0.0" width="404" height="208"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="siR-fL-v2a">
<rect key="frame" x="18" y="159" width="92" height="29"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="General" id="ENU-js-huy">
<font key="font" metaFont="systemBold" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9Gu-aU-Td2">
<rect key="frame" x="18" y="86" width="199" height="18"/>
<buttonCell key="cell" type="check" title="Start MonitorControl at Login" bezelStyle="regularSquare" imagePosition="left" inset="2" id="j72-NF-zsW">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="startAtLoginClicked:" target="BGD-tY-Myx" id="QvH-5O-Lck"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xGa-qG-9ut">
<rect key="frame" x="18" y="52" width="177" height="18"/>
<buttonCell key="cell" type="check" title="Show a slider for contrast" bezelStyle="regularSquare" imagePosition="left" inset="2" id="xSI-8W-Xd0">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="showContrastSliderClicked:" target="BGD-tY-Myx" id="Jek-jL-YMn"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="i5K-Cd-wxm">
<rect key="frame" x="18" y="18" width="215" height="18"/>
<buttonCell key="cell" type="check" title="Lower Contrast after Brightness" bezelStyle="regularSquare" imagePosition="left" inset="2" id="fhy-Er-0aI">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="lowerContrastClicked:" target="BGD-tY-Myx" id="iTd-hI-jbv"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="tMg-qE-zTW">
<rect key="frame" x="18" y="122" width="99" height="17"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MonitorControl " id="mBs-6m-13Q">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="i5K-Cd-wxm" secondAttribute="trailing" constant="20" id="7p3-HQ-gLl"/>
<constraint firstItem="xGa-qG-9ut" firstAttribute="top" secondItem="9Gu-aU-Td2" secondAttribute="bottom" constant="20" id="DOL-i0-bCI"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="9Gu-aU-Td2" secondAttribute="trailing" constant="20" id="Gft-KX-rdy"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="xGa-qG-9ut" secondAttribute="trailing" constant="20" id="HDL-zX-2dq"/>
<constraint firstItem="9Gu-aU-Td2" firstAttribute="leading" secondItem="COE-Oc-gZs" secondAttribute="leading" constant="20" id="MwQ-M8-u18"/>
<constraint firstItem="xGa-qG-9ut" firstAttribute="leading" secondItem="COE-Oc-gZs" secondAttribute="leading" constant="20" id="NgS-ga-iNo"/>
<constraint firstItem="i5K-Cd-wxm" firstAttribute="top" secondItem="xGa-qG-9ut" secondAttribute="bottom" constant="20" id="R2I-Ko-ZJ9"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="siR-fL-v2a" secondAttribute="trailing" constant="20" id="Srs-vY-IQB"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="tMg-qE-zTW" secondAttribute="trailing" constant="20" id="cf5-i9-1VX"/>
<constraint firstItem="siR-fL-v2a" firstAttribute="leading" secondItem="COE-Oc-gZs" secondAttribute="leading" constant="20" id="eXs-op-WJj"/>
<constraint firstItem="tMg-qE-zTW" firstAttribute="top" secondItem="siR-fL-v2a" secondAttribute="bottom" constant="20" id="gY5-7g-9Zb"/>
<constraint firstItem="siR-fL-v2a" firstAttribute="top" secondItem="COE-Oc-gZs" secondAttribute="top" constant="20" id="iTz-r7-NFb"/>
<constraint firstItem="i5K-Cd-wxm" firstAttribute="leading" secondItem="COE-Oc-gZs" secondAttribute="leading" constant="20" id="jWI-DQ-WdT"/>
<constraint firstItem="tMg-qE-zTW" firstAttribute="leading" secondItem="COE-Oc-gZs" secondAttribute="leading" constant="20" id="l4b-at-imk"/>
<constraint firstItem="9Gu-aU-Td2" firstAttribute="top" secondItem="tMg-qE-zTW" secondAttribute="bottom" constant="20" id="mre-Oy-gJw"/>
<constraint firstAttribute="bottom" secondItem="i5K-Cd-wxm" secondAttribute="bottom" constant="20" id="xxz-YT-Uma"/>
</constraints>
</view>
<connections>
<outlet property="lowerContrast" destination="i5K-Cd-wxm" id="sE8-Ra-JSn"/>
<outlet property="showContrastSlider" destination="xGa-qG-9ut" id="AzL-sx-Z8Q"/>
<outlet property="startAtLogin" destination="9Gu-aU-Td2" id="Tyx-Ub-Cyf"/>
<outlet property="versionLabel" destination="tMg-qE-zTW" id="31N-s8-URL"/>
</connections>
</viewController>
<customObject id="JDw-du-OST" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="162" y="218"/>
</scene>
</scenes>
</document>

View file

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.2.1</string>
<string>1.3.0</string>
<key>CFBundleVersion</key>
<string>45</string>
<string>48</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>

View file

@ -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] = []

View file

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

View file

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

View file

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

View file

@ -7,4 +7,5 @@ target 'MonitorControl' do
pod 'MediaKeyTap', :git => 'https://github.com/the0neyouseek/MediaKeyTap.git'
pod 'MASPreferences'
pod 'AMCoreAudio', '~> 3.2'
end

View file

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

View file

@ -1,13 +1,45 @@
# MonitorControl
<h1 align="center"> MonitorControl </h1>
Control your external monitor brightness, contrast or volume directly from a menulet or with keyboard native keys
<!-- subtext -->
<div align="center">
Control your external monitor brightness, contrast or volume directly from a menulet or with keyboard native keys.
</div>
![MonitorControl menulet](./.github/menulet.png)
<br/>
*Bonus: Using keyboard keys display the native osd :*
<!-- shields -->
<div align="center">
<!-- downloads -->
<a href="https://github.com/the0neyouseek/MonitorControl/releases">
<img src="https://img.shields.io/github/downloads/the0neyouseek/MonitorControl/total.svg" alt="downloads"/>
</a>
<!-- version -->
<a href="https://github.com/the0neyouseek/MonitorControl/releases/latest">
<img src="https://img.shields.io/github/release/the0neyouseek/MonitorControl.svg" alt="latest version"/>
</a>
<!-- license -->
<a href="https://github.com/the0neyouseek/MonitorControl/blob/master/License.txt">
<img src="https://img.shields.io/github/license/the0neyouseek/MonitorControl.svg" alt="license"/>
</a>
<!-- platform -->
<a href="https://github.com/the0neyouseek/MonitorControl">
<img src="https://img.shields.io/badge/platform-macOS-lightgrey.svg" alt="platform"/>
</a>
</div>
![MonitorControl OSD](./.github/osd.png)
<br/>
<div align="center">
<img src="./.github/menulet.png" alt="menulet screenshot"/>
<br/><br/>
<img src="./.github/menugeneral.png" width="299" alt="general screenshot"/><img src="./.github/menukeys.png" width="299" alt="keys screenshot"/><img src="./.github/menudisplay.png" width="299" alt="display screenshot"/>
<br/>
*Bonus: Using keyboard keys displays the native osd*
<img src="./.github/osd.jpg" width="500" align="center" alt="osd screenshot"/>
</div>
## 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)
- [@nhurden](https://github.com/nhurden/) (For the original MediaKeyTap)
- [@kfix](https://github.com/kfix/ddcctl) (For ddcctl)