Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions MonitorControl/Enums/PrefKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions MonitorControl/Model/OtherDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
42 changes: 21 additions & 21 deletions MonitorControl/Support/DisplayManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down Expand Up @@ -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)
Expand All @@ -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() {
Expand Down Expand Up @@ -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] {
Expand Down Expand Up @@ -313,7 +312,7 @@ class DisplayManager {
func clearDisplays() {
self.displays = []
}

func addDisplayCounterSuffixes() {
var nameDisplays: [String: [Display]] = [:]
for display in self.displays {
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions MonitorControl/Support/MenuHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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")
Expand Down
14 changes: 13 additions & 1 deletion MonitorControl/Support/SliderHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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()
Expand Down Expand Up @@ -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))
}
Expand Down
15 changes: 15 additions & 0 deletions MonitorControl/UI/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down Expand Up @@ -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"/>
Expand Down Expand Up @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions MonitorControl/UI/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down