mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-05-15 14:15:55 -06:00
213 lines
8.2 KiB
Swift
213 lines
8.2 KiB
Swift
import Cocoa
|
|
import DDC
|
|
import os.log
|
|
import ServiceManagement
|
|
|
|
class Utils: NSObject {
|
|
// MARK: - Menu
|
|
|
|
/// Create a slider and add it to the menu
|
|
///
|
|
/// - 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: ExternalDisplay, command: DDC.Command, title: String) -> SliderHandler {
|
|
let item = NSMenuItem()
|
|
|
|
let handler = SliderHandler(display: display, command: command)
|
|
|
|
let slider = NSSlider(value: 0, minValue: 0, maxValue: 100, target: handler, action: #selector(SliderHandler.valueChanged))
|
|
slider.isEnabled = false
|
|
handler.slider = slider
|
|
|
|
if #available(macOS 11.0, *) {
|
|
slider.frame.size.width = 160
|
|
slider.frame.origin = NSPoint(x: 35, y: 5)
|
|
let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 47, height: slider.frame.height + 14))
|
|
view.frame.origin = NSPoint(x: 12, y: 0)
|
|
var iconName: String = "circle.dashed"
|
|
switch command {
|
|
case .audioSpeakerVolume: iconName = "speaker.wave.2"
|
|
case .brightness: iconName = "sun.max"
|
|
case .contrast: iconName = "circle.lefthalf.fill"
|
|
default: break
|
|
}
|
|
let icon = NSImageView(image: NSImage(systemSymbolName: iconName, accessibilityDescription: title)!)
|
|
icon.frame = view.frame
|
|
icon.wantsLayer = true
|
|
icon.alphaValue = 0.7
|
|
icon.imageAlignment = NSImageAlignment.alignLeft
|
|
view.addSubview(icon)
|
|
view.addSubview(slider)
|
|
item.view = view
|
|
menu.insertItem(item, at: 0)
|
|
} else {
|
|
slider.frame.size.width = 180
|
|
slider.frame.origin = NSPoint(x: 15, y: 5)
|
|
let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30, height: slider.frame.height + 10))
|
|
let sliderHeaderItem = NSMenuItem()
|
|
let attrs: [NSAttributedString.Key: Any] = [.foregroundColor: NSColor.systemGray, .font: NSFont.systemFont(ofSize: 12)]
|
|
sliderHeaderItem.attributedTitle = NSAttributedString(string: title, attributes: attrs)
|
|
view.addSubview(slider)
|
|
item.view = view
|
|
menu.insertItem(item, at: 0)
|
|
menu.insertItem(sliderHeaderItem, at: 0)
|
|
}
|
|
|
|
var values: (UInt16, UInt16)?
|
|
let delay = display.needsLongerDelay ? UInt64(40 * kMillisecondScale) : nil
|
|
|
|
let tries = UInt(display.getPollingCount())
|
|
os_log("Polling %{public}@ times", type: .info, String(tries))
|
|
|
|
if tries != 0 {
|
|
values = display.readDDCValues(for: command, tries: tries, minReplyDelay: delay)
|
|
}
|
|
|
|
let (currentDDCValue, maxValue) = values ?? (UInt16(display.getValue(for: command)), 0) // We set 0 for max. value to indicate that there is no real DDC reported max. value - ExternalDisplay.getMaxValue() will return 100 in case of 0 max. values.
|
|
|
|
display.saveMaxValue(Int(maxValue), for: command)
|
|
display.saveValue(min(Int(currentDDCValue), display.getMaxValue(for: command)), for: command) // We won't allow currrent value to be higher than the max. value
|
|
|
|
os_log("%{public}@ (%{public}@):", type: .info, display.name, String(reflecting: command))
|
|
os_log(" - current ddc value: %{public}@ - from display? %{public}@", type: .info, String(currentDDCValue), String(values != nil))
|
|
os_log(" - maximum ddc value: %{public}@ - from display? %{public}@", type: .info, String(display.getMaxValue(for: command)), String(values != nil))
|
|
|
|
if command != .audioSpeakerVolume {
|
|
slider.integerValue = Int(currentDDCValue)
|
|
slider.maxValue = Double(display.getMaxValue(for: command))
|
|
} else {
|
|
// If we're looking at the audio speaker volume, also retrieve the values for the mute command
|
|
var muteValues: (current: UInt16, max: UInt16)?
|
|
|
|
os_log("Polling %{public}@ times", type: .info, String(tries))
|
|
os_log("%{public}@ (%{public}@):", type: .info, display.name, String(reflecting: DDC.Command.audioMuteScreenBlank))
|
|
|
|
if tries != 0 {
|
|
muteValues = display.readDDCValues(for: .audioMuteScreenBlank, tries: tries, minReplyDelay: delay)
|
|
}
|
|
|
|
if let muteValues = muteValues {
|
|
os_log(" - current ddc value: %{public}@", type: .info, String(muteValues.current))
|
|
os_log(" - maximum ddc value: %{public}@", type: .info, String(muteValues.max))
|
|
|
|
display.saveValue(Int(muteValues.current), for: .audioMuteScreenBlank)
|
|
display.saveMaxValue(Int(muteValues.max), for: .audioMuteScreenBlank)
|
|
} else {
|
|
os_log(" - current ddc value: unknown", type: .info)
|
|
os_log(" - stored maximum ddc value: %{public}@", type: .info, String(display.getMaxValue(for: .audioMuteScreenBlank)))
|
|
}
|
|
|
|
// If the system is not currently muted, or doesn't support the mute command, display the current volume as the slider value
|
|
if muteValues == nil || muteValues!.current == 2 {
|
|
slider.integerValue = Int(currentDDCValue)
|
|
} else {
|
|
slider.integerValue = 0
|
|
}
|
|
|
|
slider.maxValue = Double(display.getMaxValue(for: command))
|
|
}
|
|
|
|
slider.isEnabled = true
|
|
return handler
|
|
}
|
|
|
|
// MARK: - Utilities
|
|
|
|
/// Acquire Privileges (Necessary to listen to keyboard event globally)
|
|
static func acquirePrivileges() {
|
|
if !self.readPrivileges(prompt: true) {
|
|
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()
|
|
}
|
|
}
|
|
|
|
static func readPrivileges(prompt: Bool) -> Bool {
|
|
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: prompt]
|
|
let status = AXIsProcessTrustedWithOptions(options)
|
|
os_log("Reading Accessibility privileges - Current access status %{public}@", type: .info, String(status))
|
|
return status
|
|
}
|
|
|
|
static func setStartAtLogin(enabled: Bool) {
|
|
let identifier = "\(Bundle.main.bundleIdentifier!)Helper" as CFString
|
|
SMLoginItemSetEnabled(identifier, enabled)
|
|
os_log("Toggle start at login state: %{public}@", type: .info, enabled ? "on" : "off")
|
|
}
|
|
|
|
static func getSystemPreferences() -> [String: AnyObject]? {
|
|
var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml
|
|
let plistPath = NSString(string: "~/Library/Preferences/.GlobalPreferences.plist").expandingTildeInPath
|
|
guard let plistXML = FileManager.default.contents(atPath: plistPath) else {
|
|
return nil
|
|
}
|
|
do {
|
|
return try PropertyListSerialization.propertyList(from: plistXML, options: .mutableContainersAndLeaves, format: &propertyListFormat) as? [String: AnyObject]
|
|
} catch {
|
|
os_log("Error reading system prefs plist: %{public}@", type: .info, error.localizedDescription)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func checksum(chk: UInt8, data: inout [UInt8], start: Int, end: Int) -> UInt8 {
|
|
var chkd: UInt8 = chk
|
|
for i in start ... end {
|
|
chkd ^= data[i]
|
|
}
|
|
return chkd
|
|
}
|
|
|
|
// MARK: - Enums
|
|
|
|
/// UserDefault Keys for the app prefs
|
|
enum PrefKeys: String {
|
|
/// Was the app launched once
|
|
case appAlreadyLaunched
|
|
|
|
/// Does the app start when plugged to an external monitor
|
|
case startWhenExternal
|
|
|
|
/// Keys listened for (Brightness/Volume)
|
|
case listenFor
|
|
|
|
/// Show contrast sliders
|
|
case showContrast
|
|
|
|
/// Lower contrast after brightness
|
|
case lowerContrast
|
|
|
|
/// Change Brightness/Volume for all screens
|
|
case allScreens
|
|
|
|
/// Friendly name changed
|
|
case friendlyName
|
|
|
|
/// Prefs Reset
|
|
case preferenceReset
|
|
|
|
/// Used for notification when displays are updated in DisplayManager
|
|
case displayListUpdate
|
|
}
|
|
|
|
/// Keys for the value of listenFor option
|
|
enum ListenForKeys: Int {
|
|
/// Listen for Brightness and Volume keys
|
|
case brightnessAndVolumeKeys = 0
|
|
|
|
/// Listen for Brightness keys only
|
|
case brightnessOnlyKeys = 1
|
|
|
|
/// Listen for Volume keys only
|
|
case volumeOnlyKeys = 2
|
|
|
|
/// Don't listen for any keys
|
|
case none = 3
|
|
}
|
|
}
|