Refactor color temperature handling to use VCP presets

Transitioned color temperature handling from Kelvin-based values to VCP code `0x14` (Select Color Preset). Removed legacy Kelvin-to-VCP conversion logic and deprecated unused VCP codes. Updated `Monitor` and `MonitorViewModel` to reflect this change, making `ColorTemperature` read-only in the flyout UI and configurable via the Settings UI.

Enhanced monitor capabilities detection by relying on reported VCP codes instead of trial-and-error probing. Introduced `CapabilitiesStatus` to indicate feature availability and dynamically populated color temperature presets from VCP code `0x14`.

Streamlined the UI by replacing the color temperature slider with a ComboBox in the Settings UI. Added tooltips, warnings for unavailable capabilities, and improved logging for brightness and color temperature operations.

Removed obsolete code, simplified feature detection logic, and improved code documentation. Fixed issues with unsupported VCP values and ensured consistent ordering of color presets.
This commit is contained in:
Yu Leng
2025-11-14 13:17:55 +08:00
parent 4c799b61fc
commit e645a19629
16 changed files with 523 additions and 483 deletions

View File

@@ -7,6 +7,7 @@ using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using PowerDisplay.Configuration; using PowerDisplay.Configuration;
using PowerDisplay.Core.Utils;
namespace PowerDisplay.Core.Models namespace PowerDisplay.Core.Models
{ {
@@ -16,7 +17,7 @@ namespace PowerDisplay.Core.Models
public partial class Monitor : INotifyPropertyChanged public partial class Monitor : INotifyPropertyChanged
{ {
private int _currentBrightness; private int _currentBrightness;
private int _currentColorTemperature = AppConstants.MonitorDefaults.DefaultColorTemp; private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
private bool _isAvailable = true; private bool _isAvailable = true;
/// <summary> /// <summary>
@@ -67,36 +68,39 @@ namespace PowerDisplay.Core.Models
public int MaxBrightness { get; set; } = 100; public int MaxBrightness { get; set; } = 100;
/// <summary> /// <summary>
/// Current color temperature (2000-10000K) /// Current color temperature VCP preset value (from VCP code 0x14).
/// This stores the raw VCP value (e.g., 0x05 for 6500K), not Kelvin temperature.
/// Use ColorTemperaturePresetName to get human-readable name.
/// </summary> /// </summary>
public int CurrentColorTemperature public int CurrentColorTemperature
{ {
get => _currentColorTemperature; get => _currentColorTemperature;
set set
{ {
var clamped = Math.Clamp(value, MinColorTemperature, MaxColorTemperature); if (_currentColorTemperature != value)
if (_currentColorTemperature != clamped)
{ {
_currentColorTemperature = clamped; _currentColorTemperature = value;
OnPropertyChanged(); OnPropertyChanged();
OnPropertyChanged(nameof(ColorTemperaturePresetName));
} }
} }
} }
/// <summary> /// <summary>
/// Minimum color temperature value /// Human-readable color temperature preset name (e.g., "6500K", "sRGB")
/// </summary> /// </summary>
public int MinColorTemperature { get; set; } = AppConstants.MonitorDefaults.MinColorTemp; public string ColorTemperaturePresetName =>
VcpValueNames.GetName(0x14, CurrentColorTemperature) ?? $"0x{CurrentColorTemperature:X2}";
/// <summary> /// <summary>
/// Maximum color temperature value /// Whether supports color temperature adjustment via VCP 0x14
/// </summary> /// </summary>
public int MaxColorTemperature { get; set; } = AppConstants.MonitorDefaults.MaxColorTemp; public bool SupportsColorTemperature { get; set; }
/// <summary> /// <summary>
/// Whether supports color temperature adjustment /// Capabilities detection status: "available", "unavailable", or "unknown"
/// </summary> /// </summary>
public bool SupportsColorTemperature { get; set; } = true; public string CapabilitiesStatus { get; set; } = "unknown";
/// <summary> /// <summary>
/// Whether supports contrast adjustment /// Whether supports contrast adjustment

View File

@@ -362,10 +362,9 @@ namespace PowerDisplay.Core
var monitor = GetMonitor(monitorId); var monitor = GetMonitor(monitorId);
if (monitor != null) if (monitor != null)
{ {
// Convert VCP value to approximate Kelvin temperature // Store raw VCP 0x14 preset value (e.g., 0x05 for 6500K)
// This is a rough mapping - actual values depend on monitor implementation // No Kelvin conversion - we use discrete presets
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum); monitor.CurrentColorTemperature = tempInfo.Current;
monitor.CurrentColorTemperature = kelvin;
} }
} }
} }
@@ -375,14 +374,6 @@ namespace PowerDisplay.Core
} }
} }
/// <summary>
/// Convert VCP value to approximate Kelvin temperature (uses unified converter)
/// </summary>
private static int ConvertVcpValueToKelvin(int vcpValue, int maxVcpValue)
{
return ColorTemperatureConverter.VcpToKelvin(vcpValue, maxVcpValue);
}
/// <summary> /// <summary>
/// Get monitor by ID /// Get monitor by ID
/// </summary> /// </summary>

View File

@@ -1,89 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace PowerDisplay.Core.Utils
{
/// <summary>
/// Utility class for converting between Kelvin color temperature and VCP values.
/// Centralizes temperature conversion logic to eliminate code duplication (KISS principle).
/// </summary>
public static class ColorTemperatureConverter
{
/// <summary>
/// Minimum color temperature in Kelvin (warm)
/// </summary>
public const int MinKelvin = 2000;
/// <summary>
/// Maximum color temperature in Kelvin (cool)
/// </summary>
public const int MaxKelvin = 10000;
/// <summary>
/// Convert VCP value to Kelvin temperature
/// </summary>
/// <param name="vcpValue">Current VCP value</param>
/// <param name="vcpMax">Maximum VCP value</param>
/// <returns>Temperature in Kelvin (2000-10000K)</returns>
public static int VcpToKelvin(int vcpValue, int vcpMax)
{
if (vcpMax <= 0)
{
return (MinKelvin + MaxKelvin) / 2; // Default to neutral 6000K
}
// Normalize VCP value to 0-1 range
double normalized = Math.Clamp((double)vcpValue / vcpMax, 0.0, 1.0);
// Map to Kelvin range
int kelvin = (int)(MinKelvin + (normalized * (MaxKelvin - MinKelvin)));
return Math.Clamp(kelvin, MinKelvin, MaxKelvin);
}
/// <summary>
/// Convert Kelvin temperature to VCP value
/// </summary>
/// <param name="kelvin">Temperature in Kelvin (2000-10000K)</param>
/// <param name="vcpMax">Maximum VCP value</param>
/// <returns>VCP value (0 to vcpMax)</returns>
public static int KelvinToVcp(int kelvin, int vcpMax)
{
// Clamp input to valid range
kelvin = Math.Clamp(kelvin, MinKelvin, MaxKelvin);
// Normalize kelvin to 0-1 range
double normalized = (double)(kelvin - MinKelvin) / (MaxKelvin - MinKelvin);
// Map to VCP range
int vcpValue = (int)(normalized * vcpMax);
return Math.Clamp(vcpValue, 0, vcpMax);
}
/// <summary>
/// Check if a temperature value is in valid Kelvin range
/// </summary>
public static bool IsValidKelvin(int kelvin)
{
return kelvin >= MinKelvin && kelvin <= MaxKelvin;
}
/// <summary>
/// Get a human-readable description of color temperature
/// </summary>
public static string GetTemperatureDescription(int kelvin)
{
return kelvin switch
{
< 3500 => "Warm",
< 5500 => "Neutral",
< 7500 => "Cool",
_ => "Very Cool",
};
}
}
}

View File

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using ManagedCommon; using ManagedCommon;
using PowerDisplay.Core.Interfaces; using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models; using PowerDisplay.Core.Models;
using PowerDisplay.Helpers;
using static PowerDisplay.Native.NativeConstants; using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.NativeDelegates; using static PowerDisplay.Native.NativeDelegates;
using static PowerDisplay.Native.PInvoke; using static PowerDisplay.Native.PInvoke;
@@ -29,14 +30,13 @@ namespace PowerDisplay.Native.DDC
public partial class DdcCiController : IMonitorController, IDisposable public partial class DdcCiController : IMonitorController, IDisposable
{ {
private readonly PhysicalMonitorHandleManager _handleManager = new(); private readonly PhysicalMonitorHandleManager _handleManager = new();
private readonly VcpCodeResolver _vcpResolver = new();
private readonly MonitorDiscoveryHelper _discoveryHelper; private readonly MonitorDiscoveryHelper _discoveryHelper;
private bool _disposed; private bool _disposed;
public DdcCiController() public DdcCiController()
{ {
_discoveryHelper = new MonitorDiscoveryHelper(_vcpResolver); _discoveryHelper = new MonitorDiscoveryHelper();
} }
public string Name => "DDC/CI Monitor Controller"; public string Name => "DDC/CI Monitor Controller";
@@ -63,7 +63,7 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// Get monitor brightness /// Get monitor brightness using VCP code 0x10
/// </summary> /// </summary>
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default) public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
{ {
@@ -73,29 +73,32 @@ namespace PowerDisplay.Native.DDC
var physicalHandle = GetPhysicalHandle(monitor); var physicalHandle = GetPhysicalHandle(monitor);
if (physicalHandle == IntPtr.Zero) if (physicalHandle == IntPtr.Zero)
{ {
Logger.LogDebug($"[{monitor.Id}] Invalid physical handle");
return BrightnessInfo.Invalid; return BrightnessInfo.Invalid;
} }
// First try high-level API // First try high-level API
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)) if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint minBrightness, out uint currentBrightness, out uint maxBrightness))
{ {
Logger.LogDebug($"[{monitor.Id}] Brightness via high-level API: {currentBrightness}/{maxBrightness}");
return new BrightnessInfo((int)currentBrightness, (int)minBrightness, (int)maxBrightness); return new BrightnessInfo((int)currentBrightness, (int)minBrightness, (int)maxBrightness);
} }
// Try different VCP codes // Try VCP code 0x10 (standard brightness)
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle); if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out uint current, out uint max))
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out uint current, out uint max))
{ {
Logger.LogDebug($"[{monitor.Id}] Brightness via 0x10: {current}/{max}");
return new BrightnessInfo((int)current, 0, (int)max); return new BrightnessInfo((int)current, 0, (int)max);
} }
Logger.LogWarning($"[{monitor.Id}] Failed to read brightness");
return BrightnessInfo.Invalid; return BrightnessInfo.Invalid;
}, },
cancellationToken); cancellationToken);
} }
/// <summary> /// <summary>
/// Set monitor brightness /// Set monitor brightness using VCP code 0x10
/// </summary> /// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default) public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
{ {
@@ -115,6 +118,7 @@ namespace PowerDisplay.Native.DDC
var currentInfo = GetBrightnessInfo(monitor, physicalHandle); var currentInfo = GetBrightnessInfo(monitor, physicalHandle);
if (!currentInfo.IsValid) if (!currentInfo.IsValid)
{ {
Logger.LogWarning($"[{monitor.Id}] Cannot read current brightness");
return MonitorOperationResult.Failure("Cannot read current brightness"); return MonitorOperationResult.Failure("Cannot read current brightness");
} }
@@ -123,21 +127,24 @@ namespace PowerDisplay.Native.DDC
// First try high-level API // First try high-level API
if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue)) if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue))
{ {
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via high-level API");
return MonitorOperationResult.Success(); return MonitorOperationResult.Success();
} }
// Try VCP codes // Try VCP code 0x10 (standard brightness)
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle); if (DdcCiNative.TrySetVCPFeature(physicalHandle, VcpCodeBrightness, targetValue))
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(physicalHandle, vcpCode.Value, targetValue))
{ {
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via 0x10");
return MonitorOperationResult.Success(); return MonitorOperationResult.Success();
} }
var lastError = GetLastError(); var lastError = GetLastError();
Logger.LogError($"[{monitor.Id}] Failed to set brightness, error: {lastError}");
return MonitorOperationResult.Failure($"Failed to set brightness via DDC/CI", (int)lastError); return MonitorOperationResult.Failure($"Failed to set brightness via DDC/CI", (int)lastError);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"[{monitor.Id}] Exception setting brightness: {ex.Message}");
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}"); return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
} }
}, },
@@ -169,7 +176,8 @@ namespace PowerDisplay.Native.DDC
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, 0, 100, cancellationToken); => SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, 0, 100, cancellationToken);
/// <summary> /// <summary>
/// Get monitor color temperature /// Get monitor color temperature using VCP code 0x14 (Select Color Preset)
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
/// </summary> /// </summary>
public async Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default) public async Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
{ {
@@ -178,28 +186,32 @@ namespace PowerDisplay.Native.DDC
{ {
if (monitor.Handle == IntPtr.Zero) if (monitor.Handle == IntPtr.Zero)
{ {
Logger.LogDebug($"[{monitor.Id}] Invalid handle for color temperature read");
return BrightnessInfo.Invalid; return BrightnessInfo.Invalid;
} }
// Try different VCP codes for color temperature // Try VCP code 0x14 (Select Color Preset)
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle); if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, out uint current, out uint max))
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(monitor.Handle, vcpCode.Value, out uint current, out uint max))
{ {
var presetName = VcpValueNames.GetName(0x14, (int)current);
Logger.LogInfo($"[{monitor.Id}] Color temperature via 0x14: 0x{current:X2} ({presetName})");
return new BrightnessInfo((int)current, 0, (int)max); return new BrightnessInfo((int)current, 0, (int)max);
} }
Logger.LogWarning($"[{monitor.Id}] Failed to read color temperature (0x14 not supported)");
return BrightnessInfo.Invalid; return BrightnessInfo.Invalid;
}, },
cancellationToken); cancellationToken);
} }
/// <summary> /// <summary>
/// Set monitor color temperature /// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
/// </summary> /// </summary>
/// <param name="monitor">Monitor to control</param>
/// <param name="colorTemperature">VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default) public async Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
{ {
colorTemperature = Math.Clamp(colorTemperature, 2000, 10000);
return await Task.Run( return await Task.Run(
() => () =>
{ {
@@ -210,28 +222,34 @@ namespace PowerDisplay.Native.DDC
try try
{ {
// Get current color temperature info to understand the range // Validate value is in supported list if capabilities available
var currentInfo = _vcpResolver.GetCurrentColorTemperature(monitor.Handle); var capabilities = monitor.VcpCapabilitiesInfo;
if (!currentInfo.IsValid) if (capabilities != null && capabilities.SupportsVcpCode(0x14))
{ {
return MonitorOperationResult.Failure("Cannot read current color temperature"); var supportedValues = capabilities.GetSupportedValues(0x14);
if (supportedValues?.Count > 0 && !supportedValues.Contains(colorTemperature))
{
var supportedList = string.Join(", ", supportedValues.Select(v => $"0x{v:X2}"));
Logger.LogWarning($"[{monitor.Id}] Color preset 0x{colorTemperature:X2} not in supported list: [{supportedList}]");
return MonitorOperationResult.Failure($"Color preset 0x{colorTemperature:X2} not supported by monitor");
}
} }
// Convert Kelvin temperature to VCP value // Set VCP 0x14 value
uint targetValue = _vcpResolver.ConvertKelvinToVcpValue(colorTemperature, currentInfo); var presetName = VcpValueNames.GetName(0x14, colorTemperature);
if (DdcCiNative.TrySetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, (uint)colorTemperature))
// Try to set using the best available VCP code
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode.Value, targetValue))
{ {
Logger.LogInfo($"[{monitor.Id}] Set color temperature to 0x{colorTemperature:X2} ({presetName}) via 0x14");
return MonitorOperationResult.Success(); return MonitorOperationResult.Success();
} }
var lastError = GetLastError(); var lastError = GetLastError();
Logger.LogError($"[{monitor.Id}] Failed to set color temperature, error: {lastError}");
return MonitorOperationResult.Failure($"Failed to set color temperature via DDC/CI", (int)lastError); return MonitorOperationResult.Failure($"Failed to set color temperature via DDC/CI", (int)lastError);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"[{monitor.Id}] Exception setting color temperature: {ex.Message}");
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}"); return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
} }
}, },
@@ -607,7 +625,7 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// Get brightness information (with explicit handle) /// Get brightness information using VCP code 0x10 only
/// </summary> /// </summary>
private BrightnessInfo GetBrightnessInfo(Monitor monitor, IntPtr physicalHandle) private BrightnessInfo GetBrightnessInfo(Monitor monitor, IntPtr physicalHandle)
{ {
@@ -622,9 +640,8 @@ namespace PowerDisplay.Native.DDC
return new BrightnessInfo((int)current, (int)min, (int)max); return new BrightnessInfo((int)current, (int)min, (int)max);
} }
// Try VCP codes // Try VCP code 0x10 (standard brightness)
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle); if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out current, out max))
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out current, out max))
{ {
return new BrightnessInfo((int)current, 0, (int)max); return new BrightnessInfo((int)current, 0, (int)max);
} }
@@ -651,7 +668,6 @@ namespace PowerDisplay.Native.DDC
if (!_disposed && disposing) if (!_disposed && disposing)
{ {
_handleManager?.Dispose(); _handleManager?.Dispose();
_vcpResolver?.ClearCache();
_disposed = true; _disposed = true;
} }
} }

View File

@@ -22,11 +22,8 @@ namespace PowerDisplay.Native.DDC
/// </summary> /// </summary>
public class MonitorDiscoveryHelper public class MonitorDiscoveryHelper
{ {
private readonly VcpCodeResolver _vcpResolver; public MonitorDiscoveryHelper()
public MonitorDiscoveryHelper(VcpCodeResolver vcpResolver)
{ {
_vcpResolver = vcpResolver;
} }
/// <summary> /// <summary>
@@ -183,34 +180,16 @@ namespace PowerDisplay.Native.DDC
IsAvailable = true, IsAvailable = true,
Handle = physicalMonitor.HPhysicalMonitor, Handle = physicalMonitor.HPhysicalMonitor,
DeviceKey = deviceKey, DeviceKey = deviceKey,
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.DdcCi, Capabilities = MonitorCapabilities.DdcCi,
ConnectionType = "External", ConnectionType = "External",
CommunicationMethod = "DDC/CI", CommunicationMethod = "DDC/CI",
Manufacturer = ExtractManufacturer(name), Manufacturer = ExtractManufacturer(name),
CapabilitiesStatus = "unknown",
}; };
// Check contrast support // Note: Feature detection (brightness, contrast, color temp, volume) is now done
if (DdcCiNative.TryGetVCPFeature(physicalMonitor.HPhysicalMonitor, VcpCodeContrast, out _, out _)) // in MonitorManager after capabilities string is retrieved and parsed.
{ // This ensures we rely on capabilities data rather than trial-and-error probing.
monitor.Capabilities |= MonitorCapabilities.Contrast;
}
// Check color temperature support (suppress logging for discovery)
var colorTempVcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitorId, physicalMonitor.HPhysicalMonitor);
monitor.SupportsColorTemperature = colorTempVcpCode.HasValue;
// Check volume support
if (DdcCiNative.TryGetVCPFeature(physicalMonitor.HPhysicalMonitor, VcpCodeVolume, out _, out _))
{
monitor.Capabilities |= MonitorCapabilities.Volume;
}
// Check high-level API support
if (DdcCiNative.TryGetMonitorBrightness(physicalMonitor.HPhysicalMonitor, out _, out _, out _))
{
monitor.Capabilities |= MonitorCapabilities.HighLevel;
}
return monitor; return monitor;
} }
catch (Exception ex) catch (Exception ex)
@@ -221,24 +200,20 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// Get current brightness using VCP codes /// Get current brightness using VCP code 0x10 only
/// </summary> /// </summary>
private BrightnessInfo GetCurrentBrightness(IntPtr handle) private BrightnessInfo GetCurrentBrightness(IntPtr handle)
{ {
// Try high-level API // Try high-level API first
if (DdcCiNative.TryGetMonitorBrightness(handle, out uint min, out uint current, out uint max)) if (DdcCiNative.TryGetMonitorBrightness(handle, out uint min, out uint current, out uint max))
{ {
return new BrightnessInfo((int)current, (int)min, (int)max); return new BrightnessInfo((int)current, (int)min, (int)max);
} }
// Try VCP codes // Try VCP code 0x10 (standard brightness)
byte[] vcpCodes = { VcpCodeBrightness, VcpCodeBacklightControl, VcpCodeBacklightLevelWhite, VcpCodeContrast }; if (DdcCiNative.TryGetVCPFeature(handle, VcpCodeBrightness, out current, out max))
foreach (var code in vcpCodes)
{ {
if (DdcCiNative.TryGetVCPFeature(handle, code, out current, out max)) return new BrightnessInfo((int)current, 0, (int)max);
{
return new BrightnessInfo((int)current, 0, (int)max);
}
} }
return BrightnessInfo.Invalid; return BrightnessInfo.Invalid;

View File

@@ -1,125 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using ManagedCommon;
using PowerDisplay.Core.Models;
using PowerDisplay.Core.Utils;
namespace PowerDisplay.Native.DDC
{
/// <summary>
/// Resolves and caches VCP codes for monitor controls
/// Handles brightness, color temperature, and other VCP feature codes
/// </summary>
public class VcpCodeResolver
{
private readonly Dictionary<string, byte> _cachedCodes = new();
// VCP code priority order (for brightness control)
private static readonly byte[] BrightnessVcpCodes =
{
NativeConstants.VcpCodeBrightness, // 0x10 - Standard brightness
NativeConstants.VcpCodeBacklightControl, // 0x13 - Backlight control
NativeConstants.VcpCodeBacklightLevelWhite, // 0x6B - White backlight level
NativeConstants.VcpCodeContrast, // 0x12 - Contrast as last resort
};
// VCP code priority order (for color temperature control)
// Per MCCS specification:
// - 0x0C (Color Temperature Request): Set specific color temperature preset
// - 0x0B (Color Temperature Increment): Increment color temperature value
private static readonly byte[] ColorTemperatureVcpCodes =
{
NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature (primary)
NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment (fallback)
};
/// <summary>
/// Get best VCP code for brightness control
/// </summary>
public byte? GetBrightnessVcpCode(string monitorId, IntPtr physicalHandle)
{
// Return cached best code if available
if (_cachedCodes.TryGetValue(monitorId, out var cachedCode))
{
return cachedCode;
}
// Find first working VCP code (highest priority)
foreach (var code in BrightnessVcpCodes)
{
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out _, out _))
{
// Cache and return the best (first working) code
_cachedCodes[monitorId] = code;
return code;
}
}
return null;
}
/// <summary>
/// Get best VCP code for color temperature control
/// </summary>
public byte? GetColorTemperatureVcpCode(string monitorId, IntPtr physicalHandle)
{
var cacheKey = $"{monitorId}_colortemp";
// Return cached best code if available
if (_cachedCodes.TryGetValue(cacheKey, out var cachedCode))
{
return cachedCode;
}
// Find first working VCP code (highest priority)
foreach (var code in ColorTemperatureVcpCodes)
{
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out _, out _))
{
// Cache and return the best (first working) code
_cachedCodes[cacheKey] = code;
return code;
}
}
return null;
}
/// <summary>
/// Convert Kelvin temperature to VCP value (uses unified converter)
/// </summary>
public uint ConvertKelvinToVcpValue(int kelvin, BrightnessInfo vcpRange)
{
return (uint)ColorTemperatureConverter.KelvinToVcp(kelvin, vcpRange.Maximum);
}
/// <summary>
/// Get current color temperature information
/// </summary>
public BrightnessInfo GetCurrentColorTemperature(IntPtr physicalHandle)
{
// Try different VCP codes to get color temperature
foreach (var code in ColorTemperatureVcpCodes)
{
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out uint current, out uint max))
{
return new BrightnessInfo((int)current, 0, (int)max);
}
}
return BrightnessInfo.Invalid;
}
/// <summary>
/// Clear all cached codes
/// </summary>
public void ClearCache()
{
_cachedCodes.Clear();
}
}
}

View File

@@ -12,60 +12,67 @@ namespace PowerDisplay.Native
public static class NativeConstants public static class NativeConstants
{ {
/// <summary> /// <summary>
/// VCP code: Brightness (primary usage) /// VCP code: Brightness (0x10)
/// Standard VESA MCCS brightness control.
/// This is the ONLY brightness code used by PowerDisplay.
/// </summary> /// </summary>
public const byte VcpCodeBrightness = 0x10; public const byte VcpCodeBrightness = 0x10;
/// <summary> /// <summary>
/// VCP code: Contrast /// VCP code: Contrast (0x12)
/// Standard VESA MCCS contrast control.
/// </summary> /// </summary>
public const byte VcpCodeContrast = 0x12; public const byte VcpCodeContrast = 0x12;
/// <summary> /// <summary>
/// VCP code: Backlight control (alternative brightness) /// VCP code: Backlight control (0x13)
/// OBSOLETE: PowerDisplay now uses only VcpCodeBrightness (0x10).
/// </summary> /// </summary>
[Obsolete("Use VcpCodeBrightness (0x10) only. PowerDisplay no longer uses fallback codes.")]
public const byte VcpCodeBacklightControl = 0x13; public const byte VcpCodeBacklightControl = 0x13;
/// <summary> /// <summary>
/// VCP code: White backlight level /// VCP code: White backlight level (0x6B)
/// OBSOLETE: PowerDisplay now uses only VcpCodeBrightness (0x10).
/// </summary> /// </summary>
[Obsolete("Use VcpCodeBrightness (0x10) only. PowerDisplay no longer uses fallback codes.")]
public const byte VcpCodeBacklightLevelWhite = 0x6B; public const byte VcpCodeBacklightLevelWhite = 0x6B;
/// <summary> /// <summary>
/// VCP code: Audio speaker volume /// VCP code: Audio Speaker Volume (0x62)
/// Standard VESA MCCS volume control for monitors with built-in speakers.
/// </summary> /// </summary>
public const byte VcpCodeVolume = 0x62; public const byte VcpCodeVolume = 0x62;
/// <summary> /// <summary>
/// VCP code: Audio mute /// VCP code: Audio mute (0x8D)
/// </summary> /// </summary>
public const byte VcpCodeMute = 0x8D; public const byte VcpCodeMute = 0x8D;
/// <summary> /// <summary>
/// VCP code: Color Temperature Request (0x0C) /// VCP code: Color Temperature Request (0x0C)
/// Per MCCS v2.2a specification: /// OBSOLETE: Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.
/// - Used to SET and GET specific color temperature presets
/// - Typically supports discrete values (e.g., 5000K, 6500K, 9300K)
/// - Primary method for color temperature control
/// </summary> /// </summary>
[Obsolete("Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.")]
public const byte VcpCodeColorTemperature = 0x0C; public const byte VcpCodeColorTemperature = 0x0C;
/// <summary> /// <summary>
/// VCP code: Color Temperature Increment (0x0B) /// VCP code: Color Temperature Increment (0x0B)
/// Per MCCS v2.2a specification: /// OBSOLETE: Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.
/// - Used for incremental color temperature adjustment
/// - Typically supports continuous range (0-100 or custom range)
/// - Fallback method when 0x0C is not supported
/// </summary> /// </summary>
[Obsolete("Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.")]
public const byte VcpCodeColorTemperatureIncrement = 0x0B; public const byte VcpCodeColorTemperatureIncrement = 0x0B;
/// <summary> /// <summary>
/// VCP code: Gamma correction (gamma adjustment) /// VCP code: Gamma correction (0x72)
/// </summary> /// </summary>
public const byte VcpCodeGamma = 0x72; public const byte VcpCodeGamma = 0x72;
/// <summary> /// <summary>
/// VCP code: Select color preset (color preset selection) /// VCP code: Select Color Preset (0x14)
/// Standard VESA MCCS color temperature preset selection.
/// Supports discrete values like: 0x01=sRGB, 0x04=5000K, 0x05=6500K, 0x08=9300K.
/// This is the standard method for color temperature control.
/// </summary> /// </summary>
public const byte VcpCodeSelectColorPreset = 0x14; public const byte VcpCodeSelectColorPreset = 0x14;

View File

@@ -195,50 +195,7 @@
Text="{x:Bind Brightness, Mode=OneWay}" /> Text="{x:Bind Brightness, Mode=OneWay}" />
</Grid> </Grid>
<!-- Color Temperature Control --> <!-- Color Temperature Control removed - now in Settings UI -->
<Grid Height="40"
HorizontalAlignment="Stretch"
Visibility="{x:Bind ConvertBoolToVisibility(ShowColorTemperature), Mode=OneWay}">
<Grid.Transitions>
<TransitionCollection>
<EntranceThemeTransition FromVerticalOffset="10" />
<RepositionThemeTransition />
</TransitionCollection>
</Grid.Transitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Glyph="&#xE790;"
FontSize="14"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
<Slider Grid.Column="1"
HorizontalAlignment="Stretch"
MinHeight="32"
VerticalAlignment="Center"
Margin="0,2"
Minimum="0"
Maximum="100"
Value="{x:Bind ColorTemperaturePercent, Mode=OneWay}"
ValueChanged="Slider_ValueChanged"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="ColorTemperature"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
<TextBlock Grid.Column="2"
FontSize="12"
FontWeight="SemiBold"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Run Text="{x:Bind ColorTemperature, Mode=OneWay}" />
<Run Text="K" FontSize="9" />
</TextBlock>
</Grid>
<!-- Contrast Control --> <!-- Contrast Control -->
<Grid Height="40" <Grid Height="40"

View File

@@ -822,9 +822,8 @@ namespace PowerDisplay
case "Brightness": case "Brightness":
monitorVm.Brightness = finalValue; monitorVm.Brightness = finalValue;
break; break;
case "ColorTemperature":
monitorVm.ColorTemperaturePercent = finalValue; // ColorTemperature case removed - now controlled via Settings UI
break;
case "Contrast": case "Contrast":
monitorVm.ContrastPercent = finalValue; monitorVm.ContrastPercent = finalValue;
break; break;

View File

@@ -498,17 +498,13 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}', skipping"); Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}', skipping");
} }
// Color temperature must be valid and within range // Color temperature: VCP 0x14 preset value (discrete values, no range check needed)
if (savedState.Value.ColorTemperature > 0 && // Note: ColorTemperature is now read-only in flyout UI, controlled via Settings UI
savedState.Value.ColorTemperature >= monitorVm.MinColorTemperature && if (savedState.Value.ColorTemperature > 0)
savedState.Value.ColorTemperature <= monitorVm.MaxColorTemperature)
{ {
// Validation will happen in Settings UI when applying preset values
monitorVm.UpdatePropertySilently(nameof(monitorVm.ColorTemperature), savedState.Value.ColorTemperature); monitorVm.UpdatePropertySilently(nameof(monitorVm.ColorTemperature), savedState.Value.ColorTemperature);
} }
else
{
Logger.LogWarning($"[Startup] Invalid color temperature value {savedState.Value.ColorTemperature} for HardwareId '{hardwareId}', skipping");
}
// Contrast validation - only apply if hardware supports it // Contrast validation - only apply if hardware supports it
if (monitorVm.ShowContrast && if (monitorVm.ShowContrast &&
@@ -649,7 +645,8 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
{ {
// Apply default values // Apply default values
monitorVm.Brightness = 30; monitorVm.Brightness = 30;
monitorVm.ColorTemperature = 6500;
// ColorTemperature is now read-only in flyout UI - controlled via Settings UI only
monitorVm.Contrast = 50; monitorVm.Contrast = 50;
monitorVm.Volume = 50; monitorVm.Volume = 50;
@@ -729,6 +726,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
{ {
var result = new Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo var result = new Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo
{ {
Code = $"0x{code:X2}",
Title = $"{info.Name} (0x{code:X2})", Title = $"{info.Name} (0x{code:X2})",
}; };
@@ -744,6 +742,15 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
.ToList(); .ToList();
result.Values = $"Values: {string.Join(", ", formattedValues)}"; result.Values = $"Values: {string.Join(", ", formattedValues)}";
result.HasValues = true; result.HasValues = true;
// Populate value list for Settings UI ComboBox
result.ValueList = info.SupportedValues
.Select(v => new Microsoft.PowerToys.Settings.UI.Library.VcpValueInfo
{
Value = $"0x{v:X2}",
Name = Core.Utils.VcpValueNames.GetName(code, v),
})
.ToList();
} }
else else
{ {

View File

@@ -28,12 +28,10 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
// Simple debouncers for each property (KISS principle - simpler than complex queue) // Simple debouncers for each property (KISS principle - simpler than complex queue)
private readonly SimpleDebouncer _brightnessDebouncer = new(300); private readonly SimpleDebouncer _brightnessDebouncer = new(300);
private readonly SimpleDebouncer _colorTempDebouncer = new(300);
private readonly SimpleDebouncer _contrastDebouncer = new(300); private readonly SimpleDebouncer _contrastDebouncer = new(300);
private readonly SimpleDebouncer _volumeDebouncer = new(300); private readonly SimpleDebouncer _volumeDebouncer = new(300);
private int _brightness; private int _brightness;
private int _colorTemperature;
private int _contrast; private int _contrast;
private int _volume; private int _volume;
private bool _isAvailable; private bool _isAvailable;
@@ -51,11 +49,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
{ {
switch (propertyName) switch (propertyName)
{ {
case nameof(ColorTemperature): // ColorTemperature removed - now controlled via Settings UI
_colorTemperature = value;
OnPropertyChanged(nameof(ColorTemperature));
OnPropertyChanged(nameof(ColorTemperaturePercent));
break;
case nameof(Brightness): case nameof(Brightness):
_brightness = value; _brightness = value;
OnPropertyChanged(nameof(Brightness)); OnPropertyChanged(nameof(Brightness));
@@ -95,29 +89,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_showContrast = monitor.SupportsContrast; _showContrast = monitor.SupportsContrast;
_showVolume = monitor.SupportsVolume; _showVolume = monitor.SupportsVolume;
// Try to get current color temperature via DDC/CI, use default if failed // Color temperature initialization removed - now controlled via Settings UI
try // The Monitor.CurrentColorTemperature stores VCP 0x14 preset value (e.g., 0x05 for 6500K)
{ // and will be initialized by MonitorManager based on capabilities
// For DDC/CI monitors that support color temperature, use 6500K as default
// The actual temperature will be loaded asynchronously after construction
if (monitor.SupportsColorTemperature)
{
_colorTemperature = 6500; // Default neutral temperature for DDC monitors
}
else
{
_colorTemperature = 6500; // Default for unsupported monitors
}
monitor.CurrentColorTemperature = _colorTemperature;
Logger.LogDebug($"Initialized {monitor.Id} with default color temperature {_colorTemperature}K");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize color temperature for {monitor.Id}: {ex.Message}");
_colorTemperature = 6500; // Default neutral temperature
monitor.CurrentColorTemperature = 6500;
}
// Initialize basic properties from monitor // Initialize basic properties from monitor
_brightness = monitor.CurrentBrightness; _brightness = monitor.CurrentBrightness;
@@ -152,10 +126,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
public int MaxBrightness => _monitor.MaxBrightness; public int MaxBrightness => _monitor.MaxBrightness;
public int MinColorTemperature => _monitor.MinColorTemperature;
public int MaxColorTemperature => _monitor.MaxColorTemperature;
public int MinContrast => _monitor.MinContrast; public int MinContrast => _monitor.MinContrast;
public int MaxContrast => _monitor.MaxContrast; public int MaxContrast => _monitor.MaxContrast;
@@ -238,41 +208,12 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
} }
} }
public int ColorTemperature /// <summary>
{ /// Color temperature VCP preset value (from VCP code 0x14).
get => _colorTemperature; /// Read-only in flyout UI - controlled via Settings UI.
set /// Returns the raw VCP value (e.g., 0x05 for 6500K).
{ /// </summary>
if (_colorTemperature != value) public int ColorTemperature => _monitor.CurrentColorTemperature;
{
_colorTemperature = value;
OnPropertyChanged();
// Debounce hardware update - simple and clean!
var capturedValue = value;
_colorTempDebouncer.Debounce(async () =>
{
try
{
var result = await _monitorManager.SetColorTemperatureAsync(Id, capturedValue);
if (result.IsSuccess)
{
_monitor.CurrentColorTemperature = capturedValue;
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "ColorTemperature", capturedValue);
}
else
{
Logger.LogError($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to set color temperature for {Id}: {ex.Message}");
}
});
}
}
}
public int Contrast public int Contrast
{ {
@@ -348,19 +289,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
} }
}); });
public ICommand SetColorTemperatureCommand => new RelayCommand<int?>((temperature) => // SetColorTemperatureCommand removed - now controlled via Settings UI
{
if (temperature.HasValue && _monitor.SupportsColorTemperature)
{
Logger.LogDebug($"[{Id}] Color temperature command: {temperature.Value}K (DDC/CI)");
ColorTemperature = temperature.Value;
}
else if (temperature.HasValue && !_monitor.SupportsColorTemperature)
{
Logger.LogWarning($"[{Id}] Color temperature not supported on this monitor");
}
});
public ICommand SetContrastCommand => new RelayCommand<int?>((contrast) => public ICommand SetContrastCommand => new RelayCommand<int?>((contrast) =>
{ {
if (contrast.HasValue) if (contrast.HasValue)
@@ -378,16 +307,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
}); });
// Percentage-based properties for uniform slider behavior // Percentage-based properties for uniform slider behavior
public int ColorTemperaturePercent // ColorTemperaturePercent removed - now controlled via Settings UI
{
get => MapToPercent(_colorTemperature, MinColorTemperature, MaxColorTemperature);
set
{
var actualValue = MapFromPercent(value, MinColorTemperature, MaxColorTemperature);
ColorTemperature = actualValue;
}
}
public int ContrastPercent public int ContrastPercent
{ {
get => MapToPercent(_contrast, MinContrast, MaxContrast); get => MapToPercent(_contrast, MinContrast, MaxContrast);
@@ -427,11 +347,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// Notify percentage properties when actual values change // Notify percentage properties when actual values change
if (propertyName == nameof(ColorTemperature)) if (propertyName == nameof(Contrast))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ColorTemperaturePercent)));
}
else if (propertyName == nameof(Contrast))
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContrastPercent))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContrastPercent)));
} }
@@ -455,7 +371,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
// Dispose all debouncers // Dispose all debouncers
_brightnessDebouncer?.Dispose(); _brightnessDebouncer?.Dispose();
_colorTempDebouncer?.Dispose();
_contrastDebouncer?.Dispose(); _contrastDebouncer?.Dispose();
_volumeDebouncer?.Dispose(); _volumeDebouncer?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -26,6 +27,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
private List<string> _vcpCodes = new List<string>(); private List<string> _vcpCodes = new List<string>();
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>(); private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
// Feature support status (determined from capabilities)
private bool _supportsBrightness = true; // Brightness always shown even if unsupported
private bool _supportsContrast;
private bool _supportsColorTemperature;
private bool _supportsVolume;
private string _capabilitiesStatus = "unknown"; // "available", "unavailable", or "unknown"
// Available color temperature presets (populated from VcpCodesFormatted for VCP 0x14)
private ObservableCollection<ColorPresetItem> _availableColorPresets = new ObservableCollection<ColorPresetItem>();
public MonitorInfo() public MonitorInfo()
{ {
} }
@@ -265,7 +276,162 @@ namespace Microsoft.PowerToys.Settings.UI.Library
} }
} }
[JsonPropertyName("supportsBrightness")]
public bool SupportsBrightness
{
get => _supportsBrightness;
set
{
if (_supportsBrightness != value)
{
_supportsBrightness = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BrightnessTooltip));
}
}
}
[JsonPropertyName("supportsContrast")]
public bool SupportsContrast
{
get => _supportsContrast;
set
{
if (_supportsContrast != value)
{
_supportsContrast = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ContrastTooltip));
}
}
}
[JsonPropertyName("supportsColorTemperature")]
public bool SupportsColorTemperature
{
get => _supportsColorTemperature;
set
{
if (_supportsColorTemperature != value)
{
_supportsColorTemperature = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorTemperatureTooltip));
}
}
}
[JsonPropertyName("supportsVolume")]
public bool SupportsVolume
{
get => _supportsVolume;
set
{
if (_supportsVolume != value)
{
_supportsVolume = value;
OnPropertyChanged();
OnPropertyChanged(nameof(VolumeTooltip));
}
}
}
[JsonPropertyName("capabilitiesStatus")]
public string CapabilitiesStatus
{
get => _capabilitiesStatus;
set
{
if (_capabilitiesStatus != value)
{
_capabilitiesStatus = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowCapabilitiesWarning));
}
}
}
[JsonPropertyName("availableColorPresets")]
public ObservableCollection<ColorPresetItem> AvailableColorPresets
{
get => _availableColorPresets;
set
{
if (_availableColorPresets != value)
{
_availableColorPresets = value ?? new ObservableCollection<ColorPresetItem>();
OnPropertyChanged();
OnPropertyChanged(nameof(HasColorPresets));
}
}
}
[JsonIgnore]
public bool HasColorPresets => _availableColorPresets != null && _availableColorPresets.Count > 0;
[JsonIgnore] [JsonIgnore]
public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw); public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
[JsonIgnore]
public bool ShowCapabilitiesWarning => _capabilitiesStatus == "unavailable";
[JsonIgnore]
public string BrightnessTooltip => _supportsBrightness ? string.Empty : "Brightness control not supported by this monitor";
[JsonIgnore]
public string ContrastTooltip => _supportsContrast ? string.Empty : "Contrast control not supported by this monitor";
[JsonIgnore]
public string ColorTemperatureTooltip => _supportsColorTemperature ? string.Empty : "Color temperature control not supported by this monitor";
[JsonIgnore]
public string VolumeTooltip => _supportsVolume ? string.Empty : "Volume control not supported by this monitor";
/// <summary>
/// Represents a color temperature preset item for VCP code 0x14
/// </summary>
public class ColorPresetItem : Observable
{
private int _vcpValue;
private string _displayName = string.Empty;
[JsonPropertyName("vcpValue")]
public int VcpValue
{
get => _vcpValue;
set
{
if (_vcpValue != value)
{
_vcpValue = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("displayName")]
public string DisplayName
{
get => _displayName;
set
{
if (_displayName != value)
{
_displayName = value;
OnPropertyChanged();
}
}
}
public ColorPresetItem()
{
}
public ColorPresetItem(int vcpValue, string displayName)
{
VcpValue = vcpValue;
DisplayName = displayName;
}
}
} }
} }

View File

@@ -11,6 +11,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// </summary> /// </summary>
public class VcpCodeDisplayInfo public class VcpCodeDisplayInfo
{ {
[JsonPropertyName("code")]
public string Code { get; set; } = string.Empty;
[JsonPropertyName("title")] [JsonPropertyName("title")]
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
@@ -19,5 +22,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("hasValues")] [JsonPropertyName("hasValues")]
public bool HasValues { get; set; } public bool HasValues { get; set; }
[JsonPropertyName("valueList")]
public System.Collections.Generic.List<VcpValueInfo> ValueList { get; set; } = new System.Collections.Generic.List<VcpValueInfo>();
} }
} }

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Individual VCP value information
/// </summary>
public class VcpValueInfo
{
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
}
}

View File

@@ -87,6 +87,23 @@
HeaderIcon="{ui:FontIcon Glyph=&#xE7F4;}" HeaderIcon="{ui:FontIcon Glyph=&#xE7F4;}"
IsExpanded="False"> IsExpanded="False">
<tkcontrols:SettingsExpander.Items> <tkcontrols:SettingsExpander.Items>
<!-- Capabilities warning -->
<tkcontrols:SettingsCard
Visibility="{x:Bind ShowCapabilitiesWarning, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Content>
<InfoBar
Title="Monitor capabilities unavailable"
Message="This monitor did not report DDC/CI capabilities. Advanced controls may be limited."
IsClosable="False"
IsOpen="True"
Severity="Warning">
<InfoBar.IconSource>
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xE7BA;" />
</InfoBar.IconSource>
</InfoBar>
</tkcontrols:SettingsCard.Content>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Name"> <tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Name">
<TextBlock Text="{x:Bind Name, Mode=OneWay}" /> <TextBlock Text="{x:Bind Name, Mode=OneWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
@@ -102,17 +119,81 @@
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Brightness"> <tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Brightness">
<TextBlock Text="{x:Bind CurrentBrightness, Mode=OneWay}" /> <TextBlock Text="{x:Bind CurrentBrightness, Mode=OneWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_ColorTemperature"> <tkcontrols:SettingsCard
<TextBlock Text="{x:Bind ColorTemperature, Mode=OneWay}" /> x:Uid="PowerDisplay_Monitor_ColorTemperature"
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
<tkcontrols:SettingsCard.Description>
<TextBlock
Text="{x:Bind ColorTemperatureTooltip, Mode=OneWay}"
Visibility="{x:Bind SupportsColorTemperature, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</tkcontrols:SettingsCard.Description>
<ComboBox
MinWidth="200"
ItemsSource="{x:Bind AvailableColorPresets, Mode=OneWay}"
SelectedValue="{x:Bind ColorTemperature, Mode=TwoWay}"
SelectedValuePath="VcpValue"
DisplayMemberPath="DisplayName"
PlaceholderText="Not available"
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind ColorTemperatureTooltip, Mode=OneWay}" />
</ToolTipService.ToolTip>
</ComboBox>
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableColorTemperature"> <tkcontrols:SettingsCard
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch" IsOn="{x:Bind EnableColorTemperature, Mode=TwoWay}" /> x:Uid="PowerDisplay_Monitor_EnableColorTemperature"
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
<tkcontrols:SettingsCard.Description>
<TextBlock
Text="{x:Bind ColorTemperatureTooltip, Mode=OneWay}"
Visibility="{x:Bind SupportsColorTemperature, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</tkcontrols:SettingsCard.Description>
<ToggleSwitch
x:Uid="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch"
IsOn="{x:Bind EnableColorTemperature, Mode=TwoWay}"
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind ColorTemperatureTooltip, Mode=OneWay}" />
</ToolTipService.ToolTip>
</ToggleSwitch>
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableContrast"> <tkcontrols:SettingsCard
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableContrast_ToggleSwitch" IsOn="{x:Bind EnableContrast, Mode=TwoWay}" /> x:Uid="PowerDisplay_Monitor_EnableContrast"
IsEnabled="{x:Bind SupportsContrast, Mode=OneWay}">
<tkcontrols:SettingsCard.Description>
<TextBlock
Text="{x:Bind ContrastTooltip, Mode=OneWay}"
Visibility="{x:Bind SupportsContrast, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</tkcontrols:SettingsCard.Description>
<ToggleSwitch
x:Uid="PowerDisplay_Monitor_EnableContrast_ToggleSwitch"
IsOn="{x:Bind EnableContrast, Mode=TwoWay}"
IsEnabled="{x:Bind SupportsContrast, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind ContrastTooltip, Mode=OneWay}" />
</ToolTipService.ToolTip>
</ToggleSwitch>
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableVolume"> <tkcontrols:SettingsCard
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableVolume_ToggleSwitch" IsOn="{x:Bind EnableVolume, Mode=TwoWay}" /> x:Uid="PowerDisplay_Monitor_EnableVolume"
IsEnabled="{x:Bind SupportsVolume, Mode=OneWay}">
<tkcontrols:SettingsCard.Description>
<TextBlock
Text="{x:Bind VolumeTooltip, Mode=OneWay}"
Visibility="{x:Bind SupportsVolume, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</tkcontrols:SettingsCard.Description>
<ToggleSwitch
x:Uid="PowerDisplay_Monitor_EnableVolume_ToggleSwitch"
IsOn="{x:Bind EnableVolume, Mode=TwoWay}"
IsEnabled="{x:Bind SupportsVolume, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind VolumeTooltip, Mode=OneWay}" />
</ToolTipService.ToolTip>
</ToggleSwitch>
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_HideMonitor"> <tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_HideMonitor">
<ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" /> <ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" />

View File

@@ -179,6 +179,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
var monitorKey = GetMonitorKey(newMonitor); var monitorKey = GetMonitorKey(newMonitor);
// Parse capabilities to determine feature support
ParseFeatureSupportFromCapabilities(newMonitor);
// Populate color temperature presets if supported
PopulateColorPresetsForMonitor(newMonitor);
// Check if we have an existing monitor with the same key // Check if we have an existing monitor with the same key
if (existingMonitors.TryGetValue(monitorKey, out var existingMonitor)) if (existingMonitors.TryGetValue(monitorKey, out var existingMonitor))
{ {
@@ -210,6 +216,110 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return monitor.InternalName ?? monitor.Name ?? string.Empty; return monitor.InternalName ?? monitor.Name ?? string.Empty;
} }
/// <summary>
/// Parse feature support from capabilities VcpCodes list
/// Sets support flags based on VCP code presence
/// </summary>
private void ParseFeatureSupportFromCapabilities(MonitorInfo monitor)
{
if (monitor == null)
{
return;
}
// Check capabilities status
if (string.IsNullOrEmpty(monitor.CapabilitiesRaw))
{
monitor.CapabilitiesStatus = "unavailable";
monitor.SupportsBrightness = false;
monitor.SupportsContrast = false;
monitor.SupportsColorTemperature = false;
monitor.SupportsVolume = false;
return;
}
monitor.CapabilitiesStatus = "available";
// Parse VCP codes to determine feature support
// VCP codes are stored as hex strings (e.g., "0x10", "10")
var vcpCodes = monitor.VcpCodes ?? new List<string>();
// Convert all VCP codes to integers for comparison
var vcpCodeInts = new HashSet<int>();
foreach (var code in vcpCodes)
{
if (int.TryParse(code.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int codeInt))
{
vcpCodeInts.Add(codeInt);
}
}
// Check for feature support based on VCP codes
// 0x10 (16): Brightness
// 0x12 (18): Contrast
// 0x14 (20): Color Temperature (Select Color Preset)
// 0x62 (98): Volume
monitor.SupportsBrightness = vcpCodeInts.Contains(0x10);
monitor.SupportsContrast = vcpCodeInts.Contains(0x12);
monitor.SupportsColorTemperature = vcpCodeInts.Contains(0x14);
monitor.SupportsVolume = vcpCodeInts.Contains(0x62);
}
/// <summary>
/// Populate color temperature presets for a monitor from VcpCodesFormatted
/// Builds the ComboBox items from VCP code 0x14 supported values
/// </summary>
private void PopulateColorPresetsForMonitor(MonitorInfo monitor)
{
if (monitor == null)
{
return;
}
if (!monitor.SupportsColorTemperature)
{
// Create new empty collection to trigger property change notification
monitor.AvailableColorPresets = new ObservableCollection<MonitorInfo.ColorPresetItem>();
return;
}
// Find VCP code 0x14 in the formatted list
var colorTempVcp = monitor.VcpCodesFormatted?.FirstOrDefault(v =>
{
if (int.TryParse(v.Code?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int code))
{
return code == 0x14;
}
return false;
});
if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
{
// No supported values found, create new empty collection
monitor.AvailableColorPresets = new ObservableCollection<MonitorInfo.ColorPresetItem>();
return;
}
// Build preset list from supported values
var presetList = new List<MonitorInfo.ColorPresetItem>();
foreach (var valueInfo in colorTempVcp.ValueList)
{
if (int.TryParse(valueInfo.Value?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int vcpValue))
{
var displayName = valueInfo.Name ?? $"0x{vcpValue:X2}";
presetList.Add(new MonitorInfo.ColorPresetItem(vcpValue, displayName));
}
}
// Sort by VCP value for consistent ordering
presetList = presetList.OrderBy(p => p.VcpValue).ToList();
// Create new collection and assign it - this triggers property setter
// which will call UpdateSelectedColorPresetFromTemperature() to sync the selection
monitor.AvailableColorPresets = new ObservableCollection<MonitorInfo.ColorPresetItem>(presetList);
}
public void Dispose() public void Dispose()
{ {
// Unsubscribe from monitor property changes // Unsubscribe from monitor property changes