mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-05-15 14:15:55 -06:00
v1 Color Temperature Control
This commit is contained in:
parent
195a8e88ec
commit
91edbcdb2d
8 changed files with 89 additions and 29 deletions
|
|
@ -33,10 +33,13 @@ enum PrefKey: String {
|
|||
// Hide brightness sliders
|
||||
case hideBrightness
|
||||
|
||||
// Show volume sliders
|
||||
// Show contrast sliders
|
||||
case showContrast
|
||||
|
||||
// Show volume sliders
|
||||
// Show color temperature sliders
|
||||
case showColorTemperature
|
||||
|
||||
// Hide volume sliders
|
||||
case hideVolume
|
||||
|
||||
// Lower via software after brightness
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ class OtherDisplay: Display {
|
|||
return intCodes
|
||||
}
|
||||
|
||||
public func writeDDCValues(command: Command, value: UInt16) {
|
||||
func writeDDCValues(command: Command, value: UInt16) {
|
||||
guard app.sleepID == 0, app.reconfigureID == 0, !self.readPrefAsBool(key: .forceSw), !self.readPrefAsBool(key: .unavailableDDC, for: command) else {
|
||||
return
|
||||
}
|
||||
|
|
@ -486,7 +486,10 @@ class OtherDisplay: Display {
|
|||
}
|
||||
let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command))
|
||||
let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command))
|
||||
let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
|
||||
var maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
|
||||
if maxDDCValue <= minDDCValue {
|
||||
maxDDCValue = Float(DDC_MAX_DETECT_LIMIT)
|
||||
}
|
||||
let curvedValue = pow(max(min(value, 1), 0), curveMultiplier)
|
||||
let deNormalizedValue = (maxDDCValue - minDDCValue) * curvedValue + minDDCValue
|
||||
var intDDCValue = UInt16(min(max(deNormalizedValue, minDDCValue), maxDDCValue))
|
||||
|
|
@ -499,7 +502,10 @@ class OtherDisplay: Display {
|
|||
func convDDCToValue(for command: Command, from: UInt16) -> Float {
|
||||
let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command))
|
||||
let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command))
|
||||
let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
|
||||
var maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
|
||||
if maxDDCValue <= minDDCValue {
|
||||
maxDDCValue = Float(DDC_MAX_DETECT_LIMIT)
|
||||
}
|
||||
let normalizedValue = ((min(max(Float(from), minDDCValue), maxDDCValue) - minDDCValue) / (maxDDCValue - minDDCValue))
|
||||
let deCurvedValue = pow(normalizedValue, 1.0 / curveMultiplier)
|
||||
var value = deCurvedValue
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import CoreGraphics
|
|||
import os.log
|
||||
|
||||
class DisplayManager {
|
||||
public static let shared = DisplayManager()
|
||||
static let shared = DisplayManager()
|
||||
|
||||
var displays: [Display] = []
|
||||
var audioControlTargetDisplays: [OtherDisplay] = []
|
||||
|
|
@ -177,6 +177,7 @@ class DisplayManager {
|
|||
let isDummy: Bool = DisplayManager.isDummy(displayID: onlineDisplayID)
|
||||
let isVirtual: Bool = DisplayManager.isVirtual(displayID: onlineDisplayID)
|
||||
if !DEBUG_SW, DisplayManager.isAppleDisplay(displayID: onlineDisplayID) { // MARK: (point of interest for testing)
|
||||
|
||||
let appleDisplay = AppleDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, serialNumber: serialNumber, isVirtual: isVirtual, isDummy: isDummy)
|
||||
os_log("Apple display found - %{public}@", type: .info, "ID: \(appleDisplay.identifier), Name: \(appleDisplay.name) (Vendor: \(appleDisplay.vendorNumber ?? 0), Model: \(appleDisplay.modelNumber ?? 0))")
|
||||
self.addDisplay(display: appleDisplay)
|
||||
|
|
@ -190,7 +191,7 @@ class DisplayManager {
|
|||
|
||||
func setupOtherDisplays(firstrun: Bool = false) {
|
||||
for otherDisplay in self.getOtherDisplays() {
|
||||
for command in [Command.audioSpeakerVolume, Command.contrast] where !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: command) && !otherDisplay.isSw() {
|
||||
for command in [Command.audioSpeakerVolume, Command.contrast, Command.colorTemperatureRequest] where !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: command) && !otherDisplay.isSw() {
|
||||
otherDisplay.setupCurrentAndMaxValues(command: command, firstrun: firstrun)
|
||||
}
|
||||
if (!otherDisplay.isSw() && !otherDisplay.readPrefAsBool(key: .unavailableDDC, for: .brightness)) || otherDisplay.isSw() {
|
||||
|
|
@ -242,36 +243,34 @@ class DisplayManager {
|
|||
|
||||
func sortDisplays() {
|
||||
// Opsiyonel: sıralamadan önce log al
|
||||
let before = displays.map { $0.name }
|
||||
let before = self.displays.map(\.name)
|
||||
os_log("Displays before sorting: %{public}@", before)
|
||||
|
||||
|
||||
// In‑place sıralama
|
||||
displays.sort { lhs, rhs in
|
||||
self.displays.sort { lhs, rhs in
|
||||
lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
|
||||
}
|
||||
|
||||
|
||||
// Opsiyonel: sıralamadan sonra log al
|
||||
let after = displays.map { $0.name }
|
||||
let after = self.displays.map(\.name)
|
||||
os_log("Displays after sorting: %{public}@", after)
|
||||
}
|
||||
|
||||
|
||||
func sortDisplaysByFriendlyName() -> [Display] {
|
||||
return displays.sorted { lhs, rhs in
|
||||
let lhsTitle = lhs.readPrefAsString(key: .friendlyName).isEmpty
|
||||
? lhs.name
|
||||
: lhs.readPrefAsString(key: .friendlyName)
|
||||
let rhsTitle = rhs.readPrefAsString(key: .friendlyName).isEmpty
|
||||
? rhs.name
|
||||
: rhs.readPrefAsString(key: .friendlyName)
|
||||
return lhsTitle.localizedStandardCompare(rhsTitle) == .orderedDescending
|
||||
}
|
||||
self.displays.sorted { lhs, rhs in
|
||||
let lhsTitle = lhs.readPrefAsString(key: .friendlyName).isEmpty
|
||||
? lhs.name
|
||||
: lhs.readPrefAsString(key: .friendlyName)
|
||||
let rhsTitle = rhs.readPrefAsString(key: .friendlyName).isEmpty
|
||||
? rhs.name
|
||||
: rhs.readPrefAsString(key: .friendlyName)
|
||||
return lhsTitle.localizedStandardCompare(rhsTitle) == .orderedDescending
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// displays dizisini sıralar ve döner
|
||||
func getAllDisplays() -> [Display] {
|
||||
return displays
|
||||
self.displays
|
||||
}
|
||||
|
||||
func getDdcCapableDisplays() -> [OtherDisplay] {
|
||||
|
|
@ -313,7 +312,7 @@ class DisplayManager {
|
|||
func clearDisplays() {
|
||||
self.displays = []
|
||||
}
|
||||
|
||||
|
||||
func addDisplayCounterSuffixes() {
|
||||
var nameDisplays: [String: [Display]] = [:]
|
||||
for display in self.displays {
|
||||
|
|
@ -549,6 +548,7 @@ class DisplayManager {
|
|||
}
|
||||
}
|
||||
if let screen = getByDisplayID(displayID: displayID) { // MARK: This, and NSScreen+Extension.swift will not be needed when we drop MacOS 10 support.
|
||||
|
||||
if #available(macOS 10.15, *) {
|
||||
return screen.localizedName
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,9 @@ class MenuHandler: NSMenu, NSMenuDelegate {
|
|||
if let sliderHandler = self.combinedSliderHandler[.audioSpeakerVolume] {
|
||||
self.addSliderItem(monitorSubMenu: self, sliderHandler: sliderHandler)
|
||||
}
|
||||
if let sliderHandler = self.combinedSliderHandler[.colorTemperatureRequest] {
|
||||
self.addSliderItem(monitorSubMenu: self, sliderHandler: sliderHandler)
|
||||
}
|
||||
if let sliderHandler = self.combinedSliderHandler[.contrast] {
|
||||
self.addSliderItem(monitorSubMenu: self, sliderHandler: sliderHandler)
|
||||
}
|
||||
|
|
@ -189,6 +192,11 @@ class MenuHandler: NSMenu, NSMenuDelegate {
|
|||
let title = NSLocalizedString("Contrast", comment: "Shown in menu")
|
||||
addedSliderHandlers.append(self.setupMenuSliderHandler(command: .contrast, display: display, title: title))
|
||||
}
|
||||
display.sliderHandler[.colorTemperatureRequest] = nil
|
||||
if let otherDisplay = display as? OtherDisplay, !otherDisplay.isSw(), !display.readPrefAsBool(key: .unavailableDDC, for: .colorTemperatureRequest), prefs.bool(forKey: PrefKey.showColorTemperature.rawValue) {
|
||||
let title = NSLocalizedString("Color Temperature", comment: "Shown in menu")
|
||||
addedSliderHandlers.append(self.setupMenuSliderHandler(command: .colorTemperatureRequest, display: display, title: title))
|
||||
}
|
||||
display.sliderHandler[.brightness] = nil
|
||||
if !display.readPrefAsBool(key: .unavailableDDC, for: .brightness), !prefs.bool(forKey: PrefKey.hideBrightness.rawValue) {
|
||||
let title = NSLocalizedString("Brightness", comment: "Shown in menu")
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ class SliderHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public init(display: Display?, command: Command, title: String = "", position _: Int = 0) {
|
||||
init(display: Display?, command: Command, title: String = "", position _: Int = 0) {
|
||||
self.command = command
|
||||
self.title = title
|
||||
let slider = SliderHandler.MCSlider(value: 0, minValue: 0, maxValue: 1, target: self, action: #selector(SliderHandler.valueChanged))
|
||||
|
|
@ -227,6 +227,7 @@ class SliderHandler {
|
|||
case .audioSpeakerVolume: iconName = "speaker.wave.2.fill"
|
||||
case .brightness: iconName = "sun.max.fill"
|
||||
case .contrast: iconName = "circle.lefthalf.fill"
|
||||
case .colorTemperatureRequest: iconName = "thermometer.medium"
|
||||
default: break
|
||||
}
|
||||
let icon = SliderHandler.ClickThroughImageView()
|
||||
|
|
@ -297,6 +298,17 @@ class SliderHandler {
|
|||
if !otherDisplay.readPrefAsBool(key: .enableMuteUnmute) || value != 0 {
|
||||
otherDisplay.writeDDCValues(command: self.command, value: otherDisplay.convValueToDDC(for: self.command, from: value))
|
||||
}
|
||||
} else if self.command == Command.colorTemperatureRequest {
|
||||
// Color temperature: adjust red and blue gains
|
||||
// Based on Kelvin to RGB conversion (Tanner Helland algorithm)
|
||||
// Blue changes more than red (asymmetric like real color temp)
|
||||
// value 0 = cool (~9300K), value 0.5 = neutral (~6500K), value 1 = warm (~2700K)
|
||||
// Red range: 40-60 (±10 from neutral 50)
|
||||
// Blue range: 30-70 (±20 from neutral 50)
|
||||
let redValue = UInt16(40 + value * 20) // 40 at cool, 50 at neutral, 60 at warm
|
||||
let blueValue = UInt16(70 - value * 40) // 70 at cool, 50 at neutral, 30 at warm
|
||||
otherDisplay.writeDDCValues(command: .videoGainRed, value: redValue)
|
||||
otherDisplay.writeDDCValues(command: .videoGainBlue, value: blueValue)
|
||||
} else {
|
||||
otherDisplay.writeDDCValues(command: self.command, value: otherDisplay.convValueToDDC(for: self.command, from: value))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@
|
|||
<gridRow bottomPadding="-6" id="d8s-SW-acK"/>
|
||||
<gridRow bottomPadding="-13" id="xbz-Tf-py0"/>
|
||||
<gridRow bottomPadding="-10" id="KPA-bi-7h3"/>
|
||||
<gridRow bottomPadding="-13" id="cTR-Ct-m7T"/>
|
||||
<gridRow bottomPadding="-13" id="CuW-77-ls4"/>
|
||||
<gridRow bottomPadding="-10" id="MaK-MK-gRQ"/>
|
||||
<gridRow bottomPadding="-10" id="7kD-xD-py0"/>
|
||||
|
|
@ -540,6 +541,19 @@
|
|||
</textFieldCell>
|
||||
</textField>
|
||||
</gridCell>
|
||||
<gridCell row="cTR-Ct-m7T" column="FRJ-Rb-RRh" xPlacement="trailing" id="cTL-Lf-9pQ"/>
|
||||
<gridCell row="cTR-Ct-m7T" column="V6M-Jv-Agj" xPlacement="leading" id="cTB-Bt-5xX">
|
||||
<button key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="cTe-mp-Sld">
|
||||
<rect key="frame" x="218" y="200" width="252" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Show color temperature slider in menu" bezelStyle="regularSquare" imagePosition="left" alignment="left" inset="2" id="cTC-ll-7vW">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="showColorTemperatureSliderClicked:" target="tLm-u5-aZ2" id="cTA-ct-8nN"/>
|
||||
</connections>
|
||||
</button>
|
||||
</gridCell>
|
||||
<gridCell row="CuW-77-ls4" column="FRJ-Rb-RRh" xPlacement="trailing" yPlacement="center" id="l3V-an-ba0">
|
||||
<textField key="contentView" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="qQD-9e-JNb">
|
||||
<rect key="frame" x="103" y="190" width="109" height="16"/>
|
||||
|
|
@ -696,6 +710,7 @@
|
|||
<outlet property="showAppleFromMenu" destination="jnZ-Nf-GaQ" id="d8s-zB-efU"/>
|
||||
<outlet property="showBrightnessSlider" destination="NaD-fA-S7k" id="QbY-GZ-Z2Z"/>
|
||||
<outlet property="showContrastSlider" destination="uYF-Oe-H5a" id="II5-CP-AzB"/>
|
||||
<outlet property="showColorTemperatureSlider" destination="cTe-mp-Sld" id="cTO-ut-9zQ"/>
|
||||
<outlet property="showTickMarks" destination="1Il-jd-K66" id="VSX-EG-XRh"/>
|
||||
<outlet property="showVolumeSlider" destination="dbn-VC-UQW" id="sHO-Yd-b3H"/>
|
||||
</connections>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
/* Shown in menu */
|
||||
"Check for updates…" = "Check for updates…";
|
||||
|
||||
/* Shown in menu */
|
||||
"Color Temperature" = "Color Temperature";
|
||||
|
||||
/* Shown in menu */
|
||||
"Contrast" = "Contrast";
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class MenuslidersPrefsViewController: NSViewController, SettingsPane {
|
|||
@IBOutlet var showAppleFromMenu: NSButton!
|
||||
@IBOutlet var showVolumeSlider: NSButton!
|
||||
@IBOutlet var showContrastSlider: NSButton!
|
||||
@IBOutlet var showColorTemperatureSlider: NSButton!
|
||||
|
||||
@IBOutlet var multiSliders: NSPopUpButton!
|
||||
|
||||
|
|
@ -95,6 +96,7 @@ class MenuslidersPrefsViewController: NSViewController, SettingsPane {
|
|||
self.showAppleFromMenu.isEnabled = false
|
||||
}
|
||||
self.showContrastSlider.state = prefs.bool(forKey: PrefKey.showContrast.rawValue) ? .on : .off
|
||||
self.showColorTemperatureSlider.state = prefs.bool(forKey: PrefKey.showColorTemperature.rawValue) ? .on : .off
|
||||
|
||||
self.multiSliders.selectItem(withTag: prefs.integer(forKey: PrefKey.multiSliders.rawValue))
|
||||
|
||||
|
|
@ -170,6 +172,17 @@ class MenuslidersPrefsViewController: NSViewController, SettingsPane {
|
|||
self.updateGridLayout()
|
||||
}
|
||||
|
||||
@IBAction func showColorTemperatureSliderClicked(_ sender: NSButton) {
|
||||
switch sender.state {
|
||||
case .on:
|
||||
prefs.set(true, forKey: PrefKey.showColorTemperature.rawValue)
|
||||
case .off:
|
||||
prefs.set(false, forKey: PrefKey.showColorTemperature.rawValue)
|
||||
default: break
|
||||
}
|
||||
app.updateMenusAndKeys()
|
||||
}
|
||||
|
||||
@IBAction func enableSliderSnapClicked(_ sender: NSButton) {
|
||||
switch sender.state {
|
||||
case .on:
|
||||
|
|
@ -211,8 +224,8 @@ class MenuslidersPrefsViewController: NSViewController, SettingsPane {
|
|||
app.updateMenusAndKeys()
|
||||
self.updateGridLayout()
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
|
||||
guard let object = object as? AnyObject else { return }
|
||||
if object === prefs, keyPath == PrefKey.menuIcon.rawValue {
|
||||
self.populateSettings()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue