Implement ctrl + brightness for controlling internal display, Respect enabled option for internal display (#175)

* Refactor display loading logic
* Split `Display` into `InternalDisplay` and `ExternalDisplay`
* Add functions for controlling internal display brightness
* Update MediaKeyTap dependency, Implement ctrl modifier for internal display
* Fix `keyRepeatTimer` issue with multiple displays while holding down a MediaKey
This commit is contained in:
Joni Van Roost 2020-02-23 12:42:57 +01:00 committed by GitHub
parent c984a8c343
commit 16837f20c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 309 additions and 206 deletions

View file

@ -1,4 +1,4 @@
github "the0neyouseek/MediaKeyTap"
github "the0neyouseek/MediaKeyTap" "master"
github "reitermarkus/DDC.swift" "master"
github "rnine/AMCoreAudio"
github "shpakovski/MASPreferences"

View file

@ -1,4 +1,4 @@
github "reitermarkus/DDC.swift" "41e7c49b0450033c5349ca1cf5234a26ebc011b8"
github "reitermarkus/DDC.swift" "1763870c94c555ff93878caaec8235fd3a9a429d"
github "rnine/AMCoreAudio" "3.3.1"
github "shpakovski/MASPreferences" "1.3"
github "the0neyouseek/MediaKeyTap" "3.1.0"
github "the0neyouseek/MediaKeyTap" "4314a361486c2907956756748939c61f460241bd"

View file

@ -34,6 +34,8 @@
6C85EFDD22CBAA8F00227EA1 /* PollingModeCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85EFDC22CBAA8F00227EA1 /* PollingModeCellView.swift */; };
6C85EFDF22CBB54100227EA1 /* PollingCountCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85EFDE22CBB54100227EA1 /* PollingCountCellView.swift */; };
6C85EFE122CC00AD00227EA1 /* NSNotification+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85EFE022CC00AD00227EA1 /* NSNotification+Extension.swift */; };
6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBFE27923DB266000D1BC41 /* Display.swift */; };
6CBFE27C23DB27A200D1BC41 /* InternalDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBFE27B23DB27A200D1BC41 /* InternalDisplay.swift */; };
6CCB278622D5315200619B05 /* HideOsdCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CCB278522D5315200619B05 /* HideOsdCellView.swift */; };
6CD444C322D4FBB8005BFD3D /* LongerDelayCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD444C222D4FBB8005BFD3D /* LongerDelayCellView.swift */; };
F01B0699228221B7008E64DB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F01B0680228221B6008E64DB /* Localizable.strings */; };
@ -42,7 +44,7 @@
F01B069F228221B7008E64DB /* SliderHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01B068F228221B7008E64DB /* SliderHandler.swift */; };
F01B06A0228221B7008E64DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F01B0690228221B7008E64DB /* Main.storyboard */; };
F01B06A1228221B7008E64DB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = F01B0692228221B7008E64DB /* MainMenu.xib */; };
F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* Display.swift */; };
F03A8DF21FFBAA6F0034DC27 /* ExternalDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* ExternalDisplay.swift */; };
F03FE4C0228DF62B001F59A4 /* FriendlyNameCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03FE4BF228DF62A001F59A4 /* FriendlyNameCellView.swift */; };
F0445D3820023E710025AE82 /* MainPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D3720023E710025AE82 /* MainPrefsViewController.swift */; };
F0445D3D200254FA0025AE82 /* KeysPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0445D3B200254FA0025AE82 /* KeysPrefsViewController.swift */; };
@ -125,6 +127,8 @@
6CAD134E23624CC1009BD53F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Main.strings; sourceTree = "<group>"; };
6CAD134F23624CC1009BD53F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/MainMenu.strings; sourceTree = "<group>"; };
6CAD135023624CC1009BD53F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
6CBFE27923DB266000D1BC41 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = "<group>"; };
6CBFE27B23DB27A200D1BC41 /* InternalDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalDisplay.swift; sourceTree = "<group>"; };
6CCB278522D5315200619B05 /* HideOsdCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideOsdCellView.swift; sourceTree = "<group>"; };
6CD444C222D4FBB8005BFD3D /* LongerDelayCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongerDelayCellView.swift; sourceTree = "<group>"; };
B0C4810623357CE500053F91 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -146,7 +150,7 @@
F01B06A522822215008E64DB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = "<group>"; };
F01B06A622822217008E64DB /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = "<group>"; };
F01B06A72282221B008E64DB /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = "<group>"; };
F03A8DF11FFBAA6F0034DC27 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = "<group>"; };
F03A8DF11FFBAA6F0034DC27 /* ExternalDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalDisplay.swift; sourceTree = "<group>"; };
F03FE4BF228DF62A001F59A4 /* FriendlyNameCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FriendlyNameCellView.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>"; };
@ -232,11 +236,11 @@
56754EAD1D9A4016007BCDC5 /* MonitorControl */ = {
isa = PBXGroup;
children = (
6C6C34F423DB25BF00C0E9CB /* Model */,
F01B0686228221B6008E64DB /* Info.plist */,
6C85EFD622C74B0E00227EA1 /* Manager */,
56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */,
56754EB01D9A4016007BCDC5 /* Assets.xcassets */,
F03A8DF11FFBAA6F0034DC27 /* Display.swift */,
28D1DDEB227FB8E9004CB494 /* Extensions */,
F01B067F228221B6008E64DB /* Support */,
F01B0687228221B6008E64DB /* UI */,
@ -245,6 +249,16 @@
path = MonitorControl;
sourceTree = "<group>";
};
6C6C34F423DB25BF00C0E9CB /* Model */ = {
isa = PBXGroup;
children = (
F03A8DF11FFBAA6F0034DC27 /* ExternalDisplay.swift */,
6CBFE27923DB266000D1BC41 /* Display.swift */,
6CBFE27B23DB27A200D1BC41 /* InternalDisplay.swift */,
);
path = Model;
sourceTree = "<group>";
};
6C85EFD622C74B0E00227EA1 /* Manager */ = {
isa = PBXGroup;
children = (
@ -512,7 +526,9 @@
6CD444C322D4FBB8005BFD3D /* LongerDelayCellView.swift in Sources */,
F03FE4C0228DF62B001F59A4 /* FriendlyNameCellView.swift in Sources */,
6C2EA1CF228F7DFB00060E3F /* PollingMode.swift in Sources */,
F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */,
6CBFE27C23DB27A200D1BC41 /* InternalDisplay.swift in Sources */,
6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */,
F03A8DF21FFBAA6F0034DC27 /* ExternalDisplay.swift in Sources */,
F0445D40200259C10025AE82 /* DisplayPrefsViewController.swift in Sources */,
28D1DDF0227FBD99004CB494 /* EDID+Extension.swift in Sources */,
6C85EFDD22CBAA8F00227EA1 /* PollingModeCellView.swift in Sources */,
@ -703,7 +719,7 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 570;
CURRENT_PROJECT_VERSION = 631;
DEVELOPMENT_TEAM = CYC8C8R4K9;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -730,7 +746,7 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 570;
CURRENT_PROJECT_VERSION = 631;
DEVELOPMENT_TEAM = CYC8C8R4K9;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -761,7 +777,7 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 570;
CURRENT_PROJECT_VERSION = 631;
DEVELOPMENT_TEAM = CYC8C8R4K9;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@ -786,7 +802,7 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 570;
CURRENT_PROJECT_VERSION = 631;
DEVELOPMENT_TEAM = CYC8C8R4K9;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;

View file

@ -77,87 +77,74 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
self.monitorItems = []
self.displayManager?.clearDisplays()
DisplayManager.shared.clearDisplays()
}
func updateDisplays() {
self.clearDisplays()
let filteredScreens = NSScreen.screens.filter { screen -> Bool in
// Skip built-in displays.
for screen in NSScreen.screens {
let name = screen.displayName ?? NSLocalizedString("Unknown", comment: "Unknown display name")
let id = screen.displayID
let vendorNumber = screen.vendorNumber
let modelNumber = screen.modelNumber
let display: Display
if screen.isBuiltin {
return false
display = InternalDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber)
} else {
display = ExternalDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber)
}
return DDC(for: screen.displayID)?.edid() != nil
DisplayManager.shared.addDisplay(display: display)
}
switch filteredScreens.count {
case 0:
// If no DDC capable display was detected
let ddcDisplays = DisplayManager.shared.getDdcCapableDisplays()
if ddcDisplays.count == 0 {
let item = NSMenuItem()
item.title = NSLocalizedString("No supported display found", comment: "Shown in menu")
item.isEnabled = false
self.monitorItems.append(item)
self.statusMenu.insertItem(item, at: 0)
self.statusMenu.insertItem(NSMenuItem.separator(), at: 1)
default:
os_log("The following supported displays were found:", type: .info)
for screen in filteredScreens {
os_log(" - %{public}@", type: .info, "\(screen.displayName ?? NSLocalizedString("Unknown", comment: "Unknown display name")) (Vendor: \(screen.vendorNumber ?? 0), Model: \(screen.modelNumber ?? 0))")
self.addScreenToMenu(screen: screen, asSubMenu: filteredScreens.count > 1)
} else {
for display in ddcDisplays {
os_log("Supported display found: %{public}@", type: .info, "\(display.name) (Vendor: \(display.vendorNumber ?? 0), Model: \(display.modelNumber ?? 0))")
self.addDisplayToMenu(display: display, asSubMenu: ddcDisplays.count > 1)
}
}
}
/// Add a screen to the menu
///
/// - Parameters:
/// - screen: The screen to add
/// - asSubMenu: Display in a sub menu or directly in menu
private func addScreenToMenu(screen: NSScreen, asSubMenu: Bool) {
let id = screen.displayID
let ddc = DDC(for: id)
private func addDisplayToMenu(display: ExternalDisplay, asSubMenu: Bool) {
let monitorSubMenu: NSMenu = asSubMenu ? NSMenu() : self.statusMenu
if let edid = ddc?.edid() {
let name = Utils.getDisplayName(forEdid: edid)
let isEnabled = (prefs.object(forKey: "\(id)-state") as? Bool) ?? true
self.statusMenu.insertItem(NSMenuItem.separator(), at: 0)
let display = Display(id, name: name, isBuiltin: screen.isBuiltin, isEnabled: isEnabled)
let monitorSubMenu: NSMenu = asSubMenu ? NSMenu() : self.statusMenu
self.statusMenu.insertItem(NSMenuItem.separator(), at: 0)
let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
forDisplay: display,
command: .audioSpeakerVolume,
title: NSLocalizedString("Volume", comment: "Shown in menu"))
let brightnessSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
forDisplay: display,
command: .brightness,
title: NSLocalizedString("Brightness", comment: "Shown in menu"))
if prefs.bool(forKey: Utils.PrefKeys.showContrast.rawValue) {
let contrastSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
forDisplay: display,
command: .contrast,
title: NSLocalizedString("Contrast", comment: "Shown in menu"))
display.contrastSliderHandler = contrastSliderHandler
}
display.volumeSliderHandler = volumeSliderHandler
display.brightnessSliderHandler = brightnessSliderHandler
self.displayManager?.addDisplay(display: display)
let monitorMenuItem = NSMenuItem()
monitorMenuItem.title = "\(display.getFriendlyName())"
if asSubMenu {
monitorMenuItem.submenu = monitorSubMenu
}
self.monitorItems.append(monitorMenuItem)
self.statusMenu.insertItem(monitorMenuItem, at: 0)
let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
forDisplay: display,
command: .audioSpeakerVolume,
title: NSLocalizedString("Volume", comment: "Shown in menu"))
let brightnessSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
forDisplay: display,
command: .brightness,
title: NSLocalizedString("Brightness", comment: "Shown in menu"))
if prefs.bool(forKey: Utils.PrefKeys.showContrast.rawValue) {
let contrastSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
forDisplay: display,
command: .contrast,
title: NSLocalizedString("Contrast", comment: "Shown in menu"))
display.contrastSliderHandler = contrastSliderHandler
}
display.volumeSliderHandler = volumeSliderHandler
display.brightnessSliderHandler = brightnessSliderHandler
let monitorMenuItem = NSMenuItem()
monitorMenuItem.title = "\(display.getFriendlyName())"
if asSubMenu {
monitorMenuItem.submenu = monitorSubMenu
}
self.monitorItems.append(monitorMenuItem)
self.statusMenu.insertItem(monitorMenuItem, at: 0)
}
private func setupViewControllers() {
@ -173,12 +160,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
advancedPrefsVc,
]
prefsController = MASPreferencesWindowController(viewControllers: views, title: NSLocalizedString("Preferences", comment: "Shown in Preferences window"))
if let displayPrefs = displayPrefsVc as? DisplayPrefsViewController {
displayPrefs.displayManager = self.displayManager
}
if let advancedPrefs = advancedPrefsVc as? AdvancedPrefsViewController {
advancedPrefs.displayManager = self.displayManager
}
}
private func subscribeEventListeners() {
@ -209,7 +190,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} else if mediaKey == .volumeDown {
return .volumeUp
}
return nil
}
}
@ -218,6 +198,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
extension AppDelegate: MediaKeyTapDelegate {
func handle(mediaKey: MediaKey, event: KeyEvent?, modifiers: NSEvent.ModifierFlags?) {
let isSmallIncrement = modifiers?.isSuperset(of: NSEvent.ModifierFlags([.shift, .option])) ?? false
// control internal display when holding ctrl modifier
let isControlModifier = modifiers?.isSuperset(of: NSEvent.ModifierFlags([.control])) ?? false
if isControlModifier, mediaKey == .brightnessUp || mediaKey == .brightnessDown {
if let internalDisplay = DisplayManager.shared.getBuiltInDisplay() as? InternalDisplay {
internalDisplay.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement)
return
}
}
let oppositeKey: MediaKey? = self.oppositeMediaKey(mediaKey: mediaKey)
let isRepeat = event?.keyRepeat ?? false
@ -229,42 +220,40 @@ extension AppDelegate: MediaKeyTapDelegate {
if isRepeat {
return
}
mediaKeyTimer.invalidate()
}
let displays = self.displayManager?.getDisplays() ?? [Display]()
guard let currentDisplay = Utils.getCurrentDisplay(from: displays) else { return }
let displays = DisplayManager.shared.getAllDisplays()
guard let currentDisplay = DisplayManager.shared.getCurrentDisplay() else { return }
let allDisplays = prefs.bool(forKey: Utils.PrefKeys.allScreens.rawValue) ? displays : [currentDisplay]
let isSmallIncrement = modifiers?.isSuperset(of: NSEvent.ModifierFlags([.shift, .option])) ?? false
// Introduce a small delay to handle the media key being held down
let delay = isRepeat ? 0.05 : 0
for display in allDisplays {
if (prefs.object(forKey: "\(display.identifier)-state") as? Bool) ?? true {
self.keyRepeatTimers[mediaKey] = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in
for display in allDisplays where display.isEnabled {
switch mediaKey {
case .brightnessUp, .brightnessDown:
self.keyRepeatTimers[mediaKey] = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in
let osdValue = display.calcNewValue(for: .brightness, isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement)
display.setBrightness(to: osdValue)
})
display.stepBrightness(isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement)
case .mute:
// The mute key should not respond to press + hold
if !isRepeat {
display.toggleMute()
// mute only matters for external displays
if let display = display as? ExternalDisplay {
display.toggleMute()
}
}
case .volumeUp, .volumeDown:
self.keyRepeatTimers[mediaKey] = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in
let osdValue = display.calcNewValue(for: .audioSpeakerVolume, isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement)
display.setVolume(to: osdValue)
})
// volume only matters for external displays
if let display = display as? ExternalDisplay {
display.stepVolume(isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement)
}
default:
return
}
}
}
})
}
// MARK: - Prefs notification
@ -304,7 +293,6 @@ extension AppDelegate: MediaKeyTapDelegate {
let keysToDelete: [MediaKey] = [.volumeUp, .volumeDown, .mute]
keys.removeAll { keysToDelete.contains($0) }
}
self.mediaKeyTap?.stop()
self.mediaKeyTap = MediaKeyTap(delegate: self, for: keys, observeBuiltIn: false)
self.mediaKeyTap?.start()
@ -312,16 +300,13 @@ extension AppDelegate: MediaKeyTapDelegate {
}
extension AppDelegate: EventSubscriber {
/**
Fires off when the default audio device changes.
*/
/// Fires off when the default audio device changes.
func eventReceiver(_ event: Event) {
if case let .defaultOutputDeviceChanged(audioDevice)? = event as? AudioHardwareEvent {
#if DEBUG
os_log("Default output device changed to “%{public}@”.", type: .info, audioDevice.name)
os_log("Can device set its own volume? %{public}@", type: .info, audioDevice.canSetVirtualMasterVolume(direction: .playback).description)
#endif
self.startOrRestartMediaKeyTap()
}
}

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>574</string>
<string>638</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>

View file

@ -1,6 +1,8 @@
import Foundation
import Cocoa
class DisplayManager {
public static let shared = DisplayManager()
private var displays: [Display] {
didSet {
NotificationCenter.default.post(name: Notification.Name(Utils.PrefKeys.displayListUpdate.rawValue), object: nil)
@ -15,10 +17,29 @@ class DisplayManager {
self.displays = displays
}
func getDisplays() -> [Display] {
func getAllDisplays() -> [Display] {
return self.displays
}
func getDdcCapableDisplays() -> [ExternalDisplay] {
return self.displays.compactMap { (display) -> ExternalDisplay? in
if let externalDisplay = display as? ExternalDisplay, externalDisplay.ddc != nil {
return externalDisplay
} else { return nil }
}
}
func getBuiltInDisplay() -> Display? {
return self.displays.first { $0 is InternalDisplay }
}
func getCurrentDisplay() -> Display? {
guard let mainDisplayID = NSScreen.main?.displayID else {
return nil
}
return self.displays.first { $0.identifier == mainDisplayID }
}
func addDisplay(display: Display) {
self.displays.append(display)
}

View file

@ -0,0 +1,70 @@
//
// Display.swift
// MonitorControl
//
// Created by Joni Van Roost on 24/01/2020.
// Copyright © 2020 Guillaume Broder. All rights reserved.
//
import DDC
import Foundation
class Display {
internal let identifier: CGDirectDisplayID
internal let name: String
internal var vendorNumber: UInt32?
internal var modelNumber: UInt32?
internal var isEnabled: Bool {
get {
return self.prefs.object(forKey: "\(self.identifier)-state") as? Bool ?? true
}
set {
self.prefs.set(newValue, forKey: "\(self.identifier)-state")
}
}
private let prefs = UserDefaults.standard
internal init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?) {
self.identifier = identifier
self.name = name
self.vendorNumber = vendorNumber
self.modelNumber = modelNumber
}
func stepBrightness(isUp _: Bool, isSmallIncrement _: Bool) {}
func setFriendlyName(_ value: String) {
self.prefs.set(value, forKey: "friendlyName-\(self.identifier)")
}
func getFriendlyName() -> String {
return self.prefs.string(forKey: "friendlyName-\(self.identifier)") ?? self.name
}
func showOsd(command: DDC.Command, value: Int, maxValue: Int = 100) {
guard let manager = OSDManager.sharedManager() as? OSDManager else {
return
}
var osdImage: Int64!
switch command {
case .brightness:
osdImage = 1 // Brightness Image
case .audioSpeakerVolume:
osdImage = 3 // Speaker image
case .audioMuteScreenBlank:
osdImage = 4 // Mute image
default:
osdImage = 1
}
manager.showImage(osdImage,
onDisplayID: self.identifier,
priority: 0x1F4,
msecUntilFade: 1000,
filledChiclets: UInt32(value),
totalChiclets: UInt32(maxValue),
locked: false)
}
}

View file

@ -3,16 +3,14 @@ import Cocoa
import DDC
import os.log
class Display {
let identifier: CGDirectDisplayID
let name: String
let isBuiltin: Bool
var isEnabled: Bool
class ExternalDisplay: Display {
var brightnessSliderHandler: SliderHandler?
var volumeSliderHandler: SliderHandler?
var contrastSliderHandler: SliderHandler?
var ddc: DDC?
private let prefs = UserDefaults.standard
var hideOsd: Bool {
get {
return self.prefs.bool(forKey: "hideOsd-\(self.identifier)")
@ -33,17 +31,12 @@ class Display {
}
}
private let prefs = UserDefaults.standard
private var audioPlayer: AVAudioPlayer?
private let osdChicletBoxes: Float = 16
init(_ identifier: CGDirectDisplayID, name: String, isBuiltin: Bool, isEnabled: Bool = true) {
self.identifier = identifier
self.name = name
self.isEnabled = isBuiltin ? false : isEnabled
override init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?) {
super.init(identifier, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber)
self.ddc = DDC(for: identifier)
self.isBuiltin = isBuiltin
}
// On some displays, the display's OSD overlaps the macOS OSD,
@ -97,7 +90,7 @@ class Display {
if !fromVolumeSlider {
self.hideDisplayOsd()
self.showOsd(command: .audioSpeakerVolume, value: volumeOSDValue)
self.showOsd(command: volumeOSDValue > 0 ? .audioSpeakerVolume : .audioMuteScreenBlank, value: volumeOSDValue)
if volumeOSDValue > 0 {
self.playVolumeChangedSound()
@ -109,8 +102,9 @@ class Display {
}
}
func setVolume(to volumeOSDValue: Int) {
func stepVolume(isUp: Bool, isSmallIncrement: Bool) {
var muteValue: Int?
let volumeOSDValue = self.calcNewValue(for: .audioSpeakerVolume, isUp: isUp, isSmallIncrement: isSmallIncrement)
let volumeDDCValue = UInt16(volumeOSDValue)
if self.isMuted(), volumeOSDValue > 0 {
@ -134,7 +128,6 @@ class Display {
return
}
}
self.saveValue(muteValue, for: .audioMuteScreenBlank)
}
@ -154,7 +147,8 @@ class Display {
}
}
func setBrightness(to osdValue: Int) {
override func stepBrightness(isUp: Bool, isSmallIncrement: Bool) {
let osdValue = Int(self.calcNewValue(for: .brightness, isUp: isUp, isSmallIncrement: isSmallIncrement))
let isAlreadySet = osdValue == self.getValue(for: .brightness)
let ddcValue = UInt16(osdValue)
@ -234,18 +228,17 @@ class Display {
let filledChicletBoxes = self.osdChicletBoxes * (Float(currentValue) / Float(self.getMaxValue(for: command)))
var nextFilledChicletBoxes: Float
var fillecChicletBoxesRel: Float = isUp ? 1 : -1
var filledChicletBoxesRel: Float = isUp ? 1 : -1
// This is a workaround to ensure that if the user has set the value using a small step (that is, the current chiclet box isn't completely filled,
// the next regular up or down step will only fill or empty that chiclet, and not the next one as well - it only really works because the max value is 100
if (isUp && ceil(filledChicletBoxes) - filledChicletBoxes > 0.15) || (!isUp && filledChicletBoxes - floor(filledChicletBoxes) > 0.15) {
fillecChicletBoxesRel = 0
filledChicletBoxesRel = 0
}
nextFilledChicletBoxes = isUp ? ceil(filledChicletBoxes + fillecChicletBoxesRel) : floor(filledChicletBoxes + fillecChicletBoxesRel)
nextFilledChicletBoxes = isUp ? ceil(filledChicletBoxes + filledChicletBoxesRel) : floor(filledChicletBoxes + filledChicletBoxesRel)
nextValue = Int(Float(self.getMaxValue(for: command)) * (nextFilledChicletBoxes / self.osdChicletBoxes))
}
return max(0, min(self.getMaxValue(for: command), Int(nextValue)))
}
@ -263,7 +256,6 @@ class Display {
func getMaxValue(for command: DDC.Command) -> Int {
let max = self.prefs.integer(forKey: "max-\(command.rawValue)-\(self.identifier)")
return max == 0 ? 100 : max
}
@ -275,14 +267,6 @@ class Display {
self.prefs.set(value, forKey: "restore-\(command.rawValue)-\(self.identifier)")
}
func setFriendlyName(_ value: String) {
self.prefs.set(value, forKey: "friendlyName-\(self.identifier)")
}
func getFriendlyName() -> String {
return self.prefs.string(forKey: "friendlyName-\(self.identifier)") ?? self.name
}
func setPollingMode(_ value: Int) {
self.prefs.set(String(value), forKey: "pollingMode-\(self.identifier)")
}
@ -327,26 +311,8 @@ class Display {
return isSmallIncrement ? 1 : Int(floor(Float(self.getMaxValue(for: command)) / self.osdChicletBoxes))
}
private func showOsd(command: DDC.Command, value: Int) {
guard let manager = OSDManager.sharedManager() as? OSDManager else {
return
}
var osdImage: Int64 = 1 // Brightness Image
if command == .audioSpeakerVolume {
osdImage = 3 // Speaker image
if self.isMuted() {
osdImage = 4 // Mute speaker
}
}
manager.showImage(osdImage,
onDisplayID: self.identifier,
priority: 0x1F4,
msecUntilFade: 1000,
filledChiclets: UInt32(value),
totalChiclets: UInt32(self.getMaxValue(for: command)),
locked: false)
override func showOsd(command: DDC.Command, value: Int, maxValue _: Int = 100) {
super.showOsd(command: command, value: value, maxValue: self.getMaxValue(for: command))
}
private func supportsMuteCommand() -> Bool {

View file

@ -0,0 +1,85 @@
//
// InternalDisplay.swift
// MonitorControl
//
// Created by Joni Van Roost on 24/01/2020.
// Copyright © 2020 Guillaume Broder. All rights reserved.
//
// Most of the code in this file was sourced from:
// https://github.com/fnesveda/ExternalDisplayBrightness
// all credit goes to @fnesveda
import Foundation
class InternalDisplay: Display {
// the queue for dispatching display operations, so they're not performed directly and concurrently
private var displayQueue: DispatchQueue
override init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?) {
self.displayQueue = DispatchQueue(label: String("displayQueue-\(identifier)"))
super.init(identifier, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber)
}
func calcNewBrightness(isUp: Bool, isSmallIncrement: Bool) -> Float {
var step: Float = (isUp ? 1 : -1) / 16.0
let delta = step / 4
if isSmallIncrement {
step = delta
}
return min(max(0, ceil((self.getBrightness() + delta) / step) * step), 1)
}
public func getBrightness() -> Float {
self.displayQueue.sync {
Float(type(of: self).CoreDisplayGetUserBrightness?(self.identifier) ?? 0.5)
}
}
override func stepBrightness(isUp: Bool, isSmallIncrement: Bool) {
let value = self.calcNewBrightness(isUp: isUp, isSmallIncrement: isSmallIncrement)
self.displayQueue.sync {
type(of: self).CoreDisplaySetUserBrightness?(self.identifier, Double(value))
type(of: self).DisplayServicesBrightnessChanged?(self.identifier, Double(value))
self.showOsd(command: .brightness, value: Int(value * 64), maxValue: 64)
}
}
// notifies the system that the brightness of a specified display has changed (to update System Preferences etc.)
// unfortunately Apple doesn't provide a public API for this, so we have to manually extract the function from the DisplayServices framework
private static var DisplayServicesBrightnessChanged: ((CGDirectDisplayID, Double) -> Void)? {
let displayServicesPath = CFURLCreateWithString(kCFAllocatorDefault, "/System/Library/PrivateFrameworks/DisplayServices.framework" as CFString, nil)
if let displayServicesBundle = CFBundleCreate(kCFAllocatorDefault, displayServicesPath) {
if let funcPointer = CFBundleGetFunctionPointerForName(displayServicesBundle, "DisplayServicesBrightnessChanged" as CFString) {
typealias DSBCFunctionType = @convention(c) (UInt32, Double) -> Void
return unsafeBitCast(funcPointer, to: DSBCFunctionType.self)
}
}
return nil
}
// reads the brightness of a display through the CoreDisplay framework
// unfortunately Apple doesn't provide a public API for this, so we have to manually extract the function from the CoreDisplay framework
private static var CoreDisplayGetUserBrightness: ((CGDirectDisplayID) -> Double)? {
let coreDisplayPath = CFURLCreateWithString(kCFAllocatorDefault, "/System/Library/Frameworks/CoreDisplay.framework" as CFString, nil)
if let coreDisplayBundle = CFBundleCreate(kCFAllocatorDefault, coreDisplayPath) {
if let funcPointer = CFBundleGetFunctionPointerForName(coreDisplayBundle, "CoreDisplay_Display_GetUserBrightness" as CFString) {
typealias CDGUBFunctionType = @convention(c) (UInt32) -> Double
return unsafeBitCast(funcPointer, to: CDGUBFunctionType.self)
}
}
return nil
}
// sets the brightness of a display through the CoreDisplay framework
// unfortunately Apple doesn't provide a public API for this, so we have to manually extract the function from the CoreDisplay framework
private static var CoreDisplaySetUserBrightness: ((CGDirectDisplayID, Double) -> Void)? {
let coreDisplayPath = CFURLCreateWithString(kCFAllocatorDefault, "/System/Library/Frameworks/CoreDisplay.framework" as CFString, nil)
if let coreDisplayBundle = CFBundleCreate(kCFAllocatorDefault, coreDisplayPath) {
if let funcPointer = CFBundleGetFunctionPointerForName(coreDisplayBundle, "CoreDisplay_Display_SetUserBrightness" as CFString) {
typealias CDSUBFunctionType = @convention(c) (UInt32, Double) -> Void
return unsafeBitCast(funcPointer, to: CDSUBFunctionType.self)
}
}
return nil
}
}

View file

@ -14,7 +14,7 @@ class Utils: NSObject {
/// - command: Command (Brightness/Volume/...)
/// - title: Title of the slider
/// - Returns: An `NSSlider` slider
static func addSliderMenuItem(toMenu menu: NSMenu, forDisplay display: Display, command: DDC.Command, title: String) -> SliderHandler {
static func addSliderMenuItem(toMenu menu: NSMenu, forDisplay display: ExternalDisplay, command: DDC.Command, title: String) -> SliderHandler {
let item = NSMenuItem()
let handler = SliderHandler(display: display, command: command)
@ -131,28 +131,6 @@ class Utils: NSObject {
}
}
// MARK: - Display Infos
/// Get the name of a display
///
/// - Parameter edid: the EDID of a display
/// - Returns: a string
static func getDisplayName(forEdid edid: EDID) -> String {
return edid.displayName() ?? NSLocalizedString("Unknown", comment: "Unknown display name")
}
/// Get the main display from a list of display
///
/// - Parameter displays: List of Display
/// - Returns: the main display or nil if not found
static func getCurrentDisplay(from displays: [Display]) -> Display? {
guard let mainDisplayID = NSScreen.main?.displayID else {
return nil
}
return displays.first { $0.identifier == mainDisplayID }
}
// MARK: - Enums
/// UserDefault Keys for the app prefs

View file

@ -4,7 +4,6 @@ import os.log
class ButtonCellView: NSTableCellView {
@IBOutlet var button: NSButton!
var display: Display?
let prefs = UserDefaults.standard
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
@ -12,17 +11,10 @@ class ButtonCellView: NSTableCellView {
@IBAction func buttonToggled(_ sender: NSButton) {
if let display = display {
switch sender.state {
case .on:
self.prefs.set(true, forKey: "\(display.identifier)-state")
case .off:
self.prefs.set(false, forKey: "\(display.identifier)-state")
default:
break
}
let isEnabled = sender.state == .on
display.isEnabled = isEnabled
#if DEBUG
os_log("Toggle enabled display state: %{public}@", type: .info, sender.state == .on ? "on" : "off")
os_log("Toggle enabled display state: %{public}@", type: .info, isEnabled ? "on" : "off")
#endif
}
}

View file

@ -22,7 +22,6 @@ class FriendlyNameCellView: NSTableCellView {
!newValue.isEmpty {
display.setFriendlyName(newValue)
NotificationCenter.default.post(name: Notification.Name(Utils.PrefKeys.friendlyName.rawValue), object: nil)
#if DEBUG
os_log("Value changed for friendly name: %{public}@", type: .info, "from `\(originalValue)` to `\(newValue)`")
#endif

View file

@ -3,7 +3,7 @@ import os.log
class HideOsdCellView: NSTableCellView {
@IBOutlet var button: NSButton!
var display: Display?
var display: ExternalDisplay?
let prefs = UserDefaults.standard
override func draw(_ dirtyRect: NSRect) {

View file

@ -3,7 +3,7 @@ import os.log
class LongerDelayCellView: NSTableCellView {
@IBOutlet var button: NSButton!
var display: Display?
var display: ExternalDisplay?
let prefs = UserDefaults.standard
override func draw(_ dirtyRect: NSRect) {

View file

@ -2,7 +2,7 @@ import Cocoa
import os.log
class PollingCountCellView: NSTableCellView {
var display: Display?
var display: ExternalDisplay?
@IBAction func valueChanged(_ sender: NSTextField) {
if let display = display {

View file

@ -11,7 +11,7 @@ import os.log
We use these tags as a way to mark selection
*/
class PollingModeCellView: NSTableCellView {
var display: Display?
var display: ExternalDisplay?
@IBOutlet var pollingModeMenu: NSPopUpButtonCell!
var didChangePollingMode: ((_ pollingModeInt: Int) -> Void)?

View file

@ -3,10 +3,10 @@ import DDC
class SliderHandler {
var slider: NSSlider?
var display: Display
var display: ExternalDisplay
let cmd: DDC.Command
public init(display: Display, command: DDC.Command) {
public init(display: ExternalDisplay, command: DDC.Command) {
self.display = display
self.cmd = command
}

View file

@ -9,8 +9,7 @@ class AdvancedPrefsViewController: NSViewController, MASPreferencesViewControlle
var toolbarItemImage: NSImage? = NSImage(named: NSImage.advancedName)
let prefs = UserDefaults.standard
var displays: [Display] = []
var displayManager: DisplayManager?
var displays: [ExternalDisplay] = []
enum DisplayColumn: Int {
case friendlyName
@ -61,10 +60,8 @@ class AdvancedPrefsViewController: NSViewController, MASPreferencesViewControlle
}
@objc func loadDisplayList() {
if let displays = displayManager?.getDisplays() {
self.displays = displays
self.displayList.reloadData()
}
self.displays = DisplayManager.shared.getDdcCapableDisplays()
self.displayList.reloadData()
}
func numberOfRows(in _: NSTableView) -> Int {
@ -119,7 +116,7 @@ class AdvancedPrefsViewController: NSViewController, MASPreferencesViewControlle
return nil
}
private func getText(for column: DisplayColumn, with display: Display) -> String {
private func getText(for column: DisplayColumn, with display: ExternalDisplay) -> String {
switch column {
case .friendlyName:
return display.getFriendlyName()

View file

@ -10,7 +10,6 @@ class DisplayPrefsViewController: NSViewController, MASPreferencesViewController
let prefs = UserDefaults.standard
var displays: [Display] = []
var displayManager: DisplayManager?
enum DisplayColumn: Int {
case checkbox
@ -57,9 +56,7 @@ class DisplayPrefsViewController: NSViewController, MASPreferencesViewController
// MARK: - Table datasource
@objc func loadDisplayList() {
if let displays = self.displayManager?.getDisplays() {
self.displays = displays
}
self.displays = DisplayManager.shared.getAllDisplays()
self.displayList.reloadData()
}
@ -82,7 +79,6 @@ class DisplayPrefsViewController: NSViewController, MASPreferencesViewController
if let cell = tableView.makeView(withIdentifier: tableColumn.identifier, owner: nil) as? ButtonCellView {
cell.display = display
cell.button.state = display.isEnabled ? .on : .off
cell.button.isEnabled = !display.isBuiltin
return cell
}
case .ddc:

View file

@ -17,11 +17,9 @@ class KeysPrefsViewController: NSViewController, MASPreferencesViewController {
@IBAction func listenForChanged(_ sender: NSPopUpButton) {
self.prefs.set(sender.selectedTag(), forKey: Utils.PrefKeys.listenFor.rawValue)
#if DEBUG
os_log("Toggle keys listened for state state: %{public}@", type: .info, sender.selectedItem?.title ?? "")
#endif
NotificationCenter.default.post(name: Notification.Name(Utils.PrefKeys.listenFor.rawValue), object: nil)
}
}

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>574</string>
<string>638</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSBackgroundOnly</key>