mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-05-15 14:15:55 -06:00
### Added/improved functionality - Added proper support for controlling Apple displays. - Added option to show/hide brightness slider. - Added option to show brightness slider for internal display and apple displays (enabled by default). - Replication of built-in and Apple display brightness to corresponding brightness slider. - Added suffix to similarly named displays for better differentiation. - Option to disable slider snapping for finer control + disable slider snapping by default. - Added option to show slider tick marks for better accuracy. - Added option to use window focus instead of mouse to determine which display to control. - Ctrl+Command+Brightness now controls external displays only (Ctrl+Brightness continues to control internal display only) - Added separate tab for menu options. - Added option to restore last saved values upon startup. - Added option for audio device name matching for display volume control selection. - Separated option to change all screens for brightness and volume. - Added option for keyboard fine scale for brightness. - Added option for keyboard fine scale for volume. - Added version check upon startup for mandatory preferences reset upon downgrade or incompatible previous version + notification about this. - Added implementation for Command+F1 macOS shortcut to enable/disable mirroring. - Added safer 'Assume last saved settings are valid' option as default instead of startup DDC read (or restore). - Streamlined preference panes, 'Show advanced settings' now affect all tabs. This leads to a better and safer first timer experience (especially because of the influx of many new features). - Added a Quit button to Preferences if menu is hidden (it was not passible to quit the application until this time in this mode only by re-enabling the menu). - Lowered default first-run volume DDC default from 75% to 15% if read is not possible or disabled to prevent unexpectedly loud sound. - Added slider skew setting on a per control basis to have the ability to manipulate DDC slider balance and OSD scale if display control is not linear. - Added the ability to set min. and max. DDC bounds on a per display, per control basis. - Audio device name override option for a display (manually assign a specific audio device to a display). - Advanced setting to invert DDC control range (some displays have the scale reversed). - Advanced setting to remap DDC control code (some displays have contrast and brightness mixed up). - Ability to mark a DDC control as available or unavailable in advanced settings under Displays. - Ability to automatically hide menu icon if there is no slider present in the menu. - Option to show slider percentage for more precision. - Option to set combined or separate OSD scale when combined hardware+software brightness is used. - Apple like smooth brightness change (both for software, hardware, mixed and DisplayServices). - Added support for DisplayLink, AirPlay, Sidecar, screen sharing etc. using window shades (this is an inferior technique to the existing software implementation - gamma control - but still better than nothing). Disabled for any kind of mirroring setups. _(Only on Big Sur and above)_ - Brightness change synchronisation from Built-In and Apple displays to other displays. This makes Touch Bar, Ambient light sensor, Control Center and System Preferences induced changes affect all displays. Synchronisation uses a sophisticated indirect delta method + the user can intervene and adjust individual screen brightness at any time to easily compensate mismatching native brightness levels. - Preferences pane tab selector has a simpler look on Catalina. - All menu sliders are now scrollable using a magic mouse/trackpad swipes or mouse wheel. - Added option for menu to show only items that are relevant to display which shows the menu currently. - Added option to enable combined sliders (note: this option combined with enabled Apple/built-in display syncing and enabled 'change all' keyboard settings finally provides full synchronised control of all displays). - Combined sliders can now display multiple displays when keyboard and brightness syncing is not enabled. _(Only on Big Sur and above)_ - Redesigned sliders to look like Big Sur/Monterey Control Center's sliders. _(Only on Big Sur and above)_ - Quit and Preferences... are now icons for a much cleaner look. _(Only on Big Sur and above)_ - Added option to change additional menu options style or hide them. _(Only on Big Sur and above)_ - Multiple displays are now in nice Big Sur styled blocks - no more ugly separators. _(Only on Big Sur and above)_ - Added customisable gamma/ddc switchover point for combined brightness in the advanced section of Displays. - Added comma separated list for control code override to enable edge cases like controlling Brightness and Contrast at the same time (use VCP list entry `10, 12` for that) - Contrast can now be controlled from keyboard via <kbd>control</kbd> + <kbd>option</kbd> + <kbd>command</kbd> + brightness up/down. - Custom keyboard shortcuts for brightness, contrast, volume and mute - Added automatic update. ### Other under the hood changes and bug fixes - Standardised internal scale among various displays and DDC ranges for ranged controls. - Uses the new internal scale for combined hardware-software brightness mode. - Migrated scales to internal float representation to prevent loss of fine detail on transformations. - Fixed double sound when muting multiple external displays at the same time. - Fixed lack of initial volume configuration if slider is not shown in menu. - Fixed wrong settings being applied to a display when replaced on Apple Silicon (UserDefaults preferences are now tied to specific display strings instead of CGDirectDisplayID - which is no longer semi-unique on arm64). - A lot of refactoring, streamlining and general optimisations.
417 lines
14 KiB
Swift
417 lines
14 KiB
Swift
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
|
|
|
|
import Cocoa
|
|
import os.log
|
|
|
|
class DisplaysPrefsCellView: NSTableCellView {
|
|
var display: Display?
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
super.draw(dirtyRect)
|
|
}
|
|
|
|
@IBOutlet var displayImage: NSImageCell!
|
|
@IBOutlet var friendlyName: NSTextFieldCell!
|
|
@IBOutlet var displayId: NSTextFieldCell!
|
|
@IBOutlet var enabledButton: NSButton!
|
|
@IBOutlet var ddcButton: NSButton!
|
|
@IBOutlet var controlMethod: NSTextFieldCell!
|
|
@IBOutlet var displayType: NSTextFieldCell!
|
|
@IBOutlet var disableVolumeOSDButton: NSButton!
|
|
|
|
@IBOutlet var advancedSettings: NSBox!
|
|
|
|
@IBOutlet var pollingModeMenu: NSPopUpButton!
|
|
@IBOutlet var longerDelayButton: NSButton!
|
|
@IBOutlet var pollingCount: NSTextFieldCell!
|
|
@IBOutlet var enableMuteButton: NSButton!
|
|
|
|
@IBOutlet var combinedBrightnessSwitchingPoint: NSSlider!
|
|
|
|
@IBOutlet var audioDeviceNameOverride: NSTextField!
|
|
@IBOutlet var updateWithCurrentAudioName: NSButton!
|
|
|
|
@IBOutlet var unavailableDDCBrightness: NSButton!
|
|
@IBOutlet var unavailableDDCVolume: NSButton!
|
|
@IBOutlet var unavailableDDCContrast: NSButton!
|
|
|
|
@IBOutlet var minDDCOverrideBrightness: NSTextField!
|
|
@IBOutlet var minDDCOverrideVolume: NSTextField!
|
|
@IBOutlet var minDDCOverrideContrast: NSTextField!
|
|
|
|
@IBOutlet var maxDDCOverrideBrightness: NSTextField!
|
|
@IBOutlet var maxDDCOverrideVolume: NSTextField!
|
|
@IBOutlet var maxDDCOverrideContrast: NSTextField!
|
|
|
|
@IBOutlet var curveDDCBrightness: NSSlider!
|
|
@IBOutlet var curveDDCVolume: NSSlider!
|
|
@IBOutlet var curveDDCContrast: NSSlider!
|
|
|
|
@IBOutlet var invertDDCBrightness: NSButton!
|
|
@IBOutlet var invertDDCVolume: NSButton!
|
|
@IBOutlet var invertDDCContrast: NSButton!
|
|
|
|
@IBOutlet var remapDDCBrightness: NSTextField!
|
|
@IBOutlet var remapDDCVolume: NSTextField!
|
|
@IBOutlet var remapDDCContrast: NSTextField!
|
|
|
|
@IBAction func openAdvancedHelp(_: NSButton) {
|
|
if let url = URL(string: "https://github.com/the0neyouseek/MonitorControl/wiki/Advanced-Preferences") {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
|
|
@IBAction func pollingModeValueChanged(_ sender: NSPopUpButton) {
|
|
if let display = display as? OtherDisplay {
|
|
let newValue = sender.selectedTag()
|
|
let originalValue = display.readPrefAsInt(key: .pollingMode)
|
|
|
|
if newValue != originalValue {
|
|
display.savePref(newValue, key: .pollingMode)
|
|
if display.readPrefAsInt(key: .pollingMode) == PollingMode.custom.rawValue {
|
|
self.pollingCount.isEnabled = true
|
|
} else {
|
|
self.pollingCount.isEnabled = false
|
|
}
|
|
self.pollingCount.stringValue = String(display.pollingCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
@IBAction func pollingCountValueChanged(_ sender: NSTextFieldCell) {
|
|
if let display = display as? OtherDisplay {
|
|
let newValue = sender.stringValue
|
|
let originalValue = "\(display.pollingCount)"
|
|
if newValue.isEmpty {
|
|
self.pollingCount.stringValue = originalValue
|
|
} else if let intValue = Int(newValue) {
|
|
self.pollingCount.stringValue = String(intValue)
|
|
} else {
|
|
self.pollingCount.stringValue = ""
|
|
}
|
|
if newValue != originalValue, !newValue.isEmpty, let newValue = Int(newValue) {
|
|
display.pollingCount = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
@IBAction func enableMuteButtonToggled(_ sender: NSButton) {
|
|
if let display = display as? OtherDisplay {
|
|
switch sender.state {
|
|
case .on:
|
|
display.savePref(true, key: .enableMuteUnmute)
|
|
case .off:
|
|
// If the display is currently muted, toggle back to unmute
|
|
// to prevent the display becoming stuck in the muted state
|
|
if display.readPrefAsInt(for: .audioMuteScreenBlank) == 1 {
|
|
display.toggleMute()
|
|
}
|
|
display.savePref(false, key: .enableMuteUnmute)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
@IBAction func longerDelayButtonToggled(_ sender: NSButton) {
|
|
if let display = self.display as? OtherDisplay {
|
|
switch sender.state {
|
|
case .on:
|
|
let alert = NSAlert()
|
|
alert.messageText = NSLocalizedString("Enable Longer Delay?", comment: "Shown in the alert dialog")
|
|
alert.informativeText = NSLocalizedString("Are you sure you want to enable a longer delay? Doing so may freeze your system and require a restart. Start at login will be disabled as a safety measure.", comment: "Shown in the alert dialog")
|
|
alert.addButton(withTitle: NSLocalizedString("Yes", comment: "Shown in the alert dialog"))
|
|
alert.addButton(withTitle: NSLocalizedString("No", comment: "Shown in the alert dialog"))
|
|
alert.alertStyle = NSAlert.Style.critical
|
|
|
|
if let window = self.window {
|
|
alert.beginSheetModal(for: window, completionHandler: { modalResponse in
|
|
if modalResponse == NSApplication.ModalResponse.alertFirstButtonReturn {
|
|
app.setStartAtLogin(enabled: false)
|
|
display.savePref(true, key: .longerDelay)
|
|
} else {
|
|
sender.state = .off
|
|
}
|
|
})
|
|
}
|
|
case .off:
|
|
display.savePref(false, key: .longerDelay)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
@IBAction func enabledButtonToggled(_ sender: NSButton) {
|
|
if let disp = display {
|
|
disp.savePref(sender.state == .off, key: .isDisabled)
|
|
}
|
|
}
|
|
|
|
@IBAction func ddcButtonToggled(_ sender: NSButton) {
|
|
if let display = display {
|
|
switch sender.state {
|
|
case .off:
|
|
display.savePref(true, key: .forceSw)
|
|
case .on:
|
|
_ = display.setDirectBrightness(1)
|
|
display.savePref(false, key: .forceSw)
|
|
default:
|
|
break
|
|
}
|
|
_ = display.setSwBrightness(1)
|
|
_ = display.setDirectBrightness(1)
|
|
let displayInfo = DisplaysPrefsViewController.getDisplayInfo(display: display)
|
|
self.controlMethod.stringValue = displayInfo.controlMethod
|
|
self.controlMethod.controlView?.toolTip = displayInfo.controlStatus
|
|
app.configure()
|
|
}
|
|
}
|
|
|
|
@IBAction func friendlyNameValueChanged(_ sender: NSTextFieldCell) {
|
|
if let display = display {
|
|
let newValue = sender.stringValue
|
|
let originalValue = (display.readPrefAsString(key: .friendlyName) != "" ? display.readPrefAsString(key: .friendlyName) : display.name)
|
|
|
|
if newValue.isEmpty {
|
|
self.friendlyName.stringValue = originalValue
|
|
return
|
|
}
|
|
|
|
if newValue != originalValue, !newValue.isEmpty {
|
|
display.savePref(newValue, key: .friendlyName)
|
|
}
|
|
app.updateMenusAndKeys()
|
|
}
|
|
}
|
|
|
|
@IBAction func disableVolumeOSDButton(_ sender: NSButton) {
|
|
if let display = display as? OtherDisplay {
|
|
switch sender.state {
|
|
case .on:
|
|
display.savePref(true, key: .hideOsd)
|
|
case .off:
|
|
display.savePref(false, key: .hideOsd)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func tagCommand(_ tag: Int) -> Command {
|
|
var command: Command
|
|
switch tag {
|
|
case 2: command = Command.audioSpeakerVolume
|
|
case 3: command = Command.contrast
|
|
default: command = Command.brightness
|
|
}
|
|
return command
|
|
}
|
|
|
|
@IBAction func combinedBrightnessSwitchingPoint(_ sender: NSSlider) {
|
|
if let display = display as? OtherDisplay {
|
|
display.savePref(Int(sender.intValue), key: .combinedBrightnessSwitchingPoint)
|
|
}
|
|
}
|
|
|
|
@IBAction func audioDeviceNameOverride(_ sender: NSTextField) {
|
|
if let display = display as? OtherDisplay {
|
|
display.savePref(sender.stringValue, key: .audioDeviceNameOverride)
|
|
}
|
|
app.configure()
|
|
}
|
|
|
|
@IBAction func updateWithCurrentAudioName(_: NSButton) {
|
|
if let defaultDevice = app.coreAudio.defaultOutputDevice {
|
|
self.audioDeviceNameOverride.stringValue = defaultDevice.name
|
|
self.audioDeviceNameOverride(self.audioDeviceNameOverride)
|
|
}
|
|
}
|
|
|
|
@IBAction func unavailableDDC(_ sender: NSButton) {
|
|
let command = self.tagCommand(sender.tag)
|
|
let prefKey = PrefKey.unavailableDDC
|
|
if let display = display as? OtherDisplay {
|
|
switch sender.state {
|
|
case .on:
|
|
display.savePref(false, key: prefKey, for: command)
|
|
case .off:
|
|
display.savePref(true, key: prefKey, for: command)
|
|
default:
|
|
break
|
|
}
|
|
_ = display.setDirectBrightness(1)
|
|
_ = display.setSwBrightness(1)
|
|
app.configure()
|
|
}
|
|
}
|
|
|
|
@IBAction func minDDCOverride(_ sender: NSTextField) {
|
|
let command = self.tagCommand(sender.tag)
|
|
let prefKey = PrefKey.minDDCOverride
|
|
let value = sender.stringValue
|
|
if let display = display as? OtherDisplay {
|
|
if let intValue = Int(value), intValue >= 0, intValue <= 65535 {
|
|
display.savePref(intValue, key: prefKey, for: command)
|
|
} else {
|
|
display.removePref(key: prefKey, for: command)
|
|
}
|
|
app.configure()
|
|
if display.prefExists(key: prefKey, for: command) {
|
|
sender.stringValue = String(display.readPrefAsInt(key: prefKey, for: command))
|
|
} else {
|
|
sender.stringValue = ""
|
|
}
|
|
} else {
|
|
sender.stringValue = ""
|
|
}
|
|
}
|
|
|
|
@IBAction func maxDDCOverride(_ sender: NSTextField) {
|
|
let command = self.tagCommand(sender.tag)
|
|
let prefKey = PrefKey.maxDDCOverride
|
|
let value = sender.stringValue
|
|
if let display = display as? OtherDisplay {
|
|
if !value.isEmpty, let intValue = UInt(value) {
|
|
display.savePref(Int(intValue), key: prefKey, for: command)
|
|
} else {
|
|
display.removePref(key: prefKey, for: command)
|
|
}
|
|
app.configure()
|
|
if display.prefExists(key: prefKey, for: command) {
|
|
sender.stringValue = String(display.readPrefAsInt(key: prefKey, for: command))
|
|
} else {
|
|
sender.stringValue = ""
|
|
}
|
|
} else {
|
|
sender.stringValue = ""
|
|
}
|
|
}
|
|
|
|
@IBAction func curveDDC(_ sender: NSSlider) {
|
|
let command = self.tagCommand(sender.tag)
|
|
let prefKey = PrefKey.curveDDC
|
|
let value = Int(sender.intValue)
|
|
if let display = display as? OtherDisplay {
|
|
display.savePref(value, key: prefKey, for: command)
|
|
}
|
|
}
|
|
|
|
@IBAction func invertDDC(_ sender: NSButton) {
|
|
let command = self.tagCommand(sender.tag)
|
|
let prefKey = PrefKey.invertDDC
|
|
if let display = display as? OtherDisplay {
|
|
switch sender.state {
|
|
case .on:
|
|
display.savePref(true, key: prefKey, for: command)
|
|
case .off:
|
|
display.savePref(false, key: prefKey, for: command)
|
|
default:
|
|
break
|
|
}
|
|
app.configure()
|
|
}
|
|
}
|
|
|
|
@IBAction func remapDDC(_ sender: NSTextField) {
|
|
let command = self.tagCommand(sender.tag)
|
|
let prefKey = PrefKey.remapDDC
|
|
let value = sender.stringValue
|
|
let values = value.components(separatedBy: ",")
|
|
var normalizedValues: [String] = []
|
|
var normalizedString: String = ""
|
|
for value in values {
|
|
let trimmedValue = value.trimmingCharacters(in: CharacterSet(charactersIn: " "))
|
|
if !trimmedValue.isEmpty, let intValue = UInt8(trimmedValue, radix: 16), intValue != 0 {
|
|
normalizedValues.append(String(format: "%02x", intValue))
|
|
}
|
|
}
|
|
var first = true
|
|
for normalizedValue in normalizedValues {
|
|
if !first {
|
|
normalizedString.append(", ")
|
|
}
|
|
normalizedString.append(normalizedValue)
|
|
first = false
|
|
}
|
|
if let display = display as? OtherDisplay {
|
|
display.savePref(normalizedString, key: prefKey, for: command)
|
|
}
|
|
sender.stringValue = normalizedString
|
|
}
|
|
|
|
@IBAction func resetSettings(_: NSButton) {
|
|
if let disp = display {
|
|
if self.ddcButton.isEnabled { // This signifies that the DDC block is enabled
|
|
self.ddcButton.state = .on
|
|
self.ddcButtonToggled(self.ddcButton)
|
|
self.enabledButton.state = .on
|
|
self.enabledButtonToggled(self.enabledButton)
|
|
self.disableVolumeOSDButton.state = .off
|
|
self.disableVolumeOSDButton(self.disableVolumeOSDButton)
|
|
self.pollingModeMenu.selectItem(withTag: 2)
|
|
self.pollingModeValueChanged(self.pollingModeMenu)
|
|
self.longerDelayButton.state = .off
|
|
self.longerDelayButtonToggled(self.longerDelayButton)
|
|
self.enableMuteButton.state = .off
|
|
self.enableMuteButtonToggled(self.enableMuteButton)
|
|
self.friendlyName.stringValue = disp.name
|
|
self.friendlyNameValueChanged(self.friendlyName)
|
|
self.combinedBrightnessSwitchingPoint.intValue = 0
|
|
self.combinedBrightnessSwitchingPoint(self.combinedBrightnessSwitchingPoint)
|
|
self.audioDeviceNameOverride.stringValue = ""
|
|
self.audioDeviceNameOverride(self.audioDeviceNameOverride)
|
|
|
|
self.unavailableDDCBrightness.state = .on
|
|
self.unavailableDDCVolume.state = .on
|
|
self.unavailableDDCContrast.state = .on
|
|
|
|
self.minDDCOverrideBrightness.stringValue = ""
|
|
self.minDDCOverrideVolume.stringValue = ""
|
|
self.minDDCOverrideContrast.stringValue = ""
|
|
|
|
self.maxDDCOverrideBrightness.stringValue = ""
|
|
self.maxDDCOverrideVolume.stringValue = ""
|
|
self.maxDDCOverrideContrast.stringValue = ""
|
|
|
|
self.curveDDCBrightness.intValue = 5
|
|
self.curveDDCVolume.intValue = 5
|
|
self.curveDDCContrast.intValue = 5
|
|
|
|
self.invertDDCBrightness.state = .off
|
|
self.invertDDCVolume.state = .off
|
|
self.invertDDCContrast.state = .off
|
|
|
|
self.remapDDCBrightness.stringValue = ""
|
|
self.remapDDCVolume.stringValue = ""
|
|
self.remapDDCContrast.stringValue = ""
|
|
|
|
self.unavailableDDC(self.unavailableDDCBrightness)
|
|
|
|
self.unavailableDDC(self.unavailableDDCBrightness)
|
|
self.unavailableDDC(self.unavailableDDCVolume)
|
|
self.unavailableDDC(self.unavailableDDCContrast)
|
|
|
|
self.minDDCOverride(self.minDDCOverrideBrightness)
|
|
self.minDDCOverride(self.minDDCOverrideVolume)
|
|
self.minDDCOverride(self.minDDCOverrideContrast)
|
|
|
|
self.maxDDCOverride(self.maxDDCOverrideBrightness)
|
|
self.maxDDCOverride(self.maxDDCOverrideVolume)
|
|
self.maxDDCOverride(self.maxDDCOverrideContrast)
|
|
|
|
self.curveDDC(self.curveDDCBrightness)
|
|
self.curveDDC(self.curveDDCVolume)
|
|
self.curveDDC(self.curveDDCContrast)
|
|
|
|
self.invertDDC(self.invertDDCBrightness)
|
|
self.invertDDC(self.invertDDCVolume)
|
|
self.invertDDC(self.invertDDCContrast)
|
|
|
|
self.remapDDC(self.remapDDCBrightness)
|
|
self.remapDDC(self.remapDDCVolume)
|
|
self.remapDDC(self.remapDDCContrast)
|
|
}
|
|
}
|
|
}
|
|
}
|