mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
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:
@@ -7,6 +7,7 @@ using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using PowerDisplay.Configuration;
|
||||
using PowerDisplay.Core.Utils;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
@@ -16,7 +17,7 @@ namespace PowerDisplay.Core.Models
|
||||
public partial class Monitor : INotifyPropertyChanged
|
||||
{
|
||||
private int _currentBrightness;
|
||||
private int _currentColorTemperature = AppConstants.MonitorDefaults.DefaultColorTemp;
|
||||
private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
|
||||
private bool _isAvailable = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -67,36 +68,39 @@ namespace PowerDisplay.Core.Models
|
||||
public int MaxBrightness { get; set; } = 100;
|
||||
|
||||
/// <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>
|
||||
public int CurrentColorTemperature
|
||||
{
|
||||
get => _currentColorTemperature;
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinColorTemperature, MaxColorTemperature);
|
||||
if (_currentColorTemperature != clamped)
|
||||
if (_currentColorTemperature != value)
|
||||
{
|
||||
_currentColorTemperature = clamped;
|
||||
_currentColorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum color temperature value
|
||||
/// Human-readable color temperature preset name (e.g., "6500K", "sRGB")
|
||||
/// </summary>
|
||||
public int MinColorTemperature { get; set; } = AppConstants.MonitorDefaults.MinColorTemp;
|
||||
public string ColorTemperaturePresetName =>
|
||||
VcpValueNames.GetName(0x14, CurrentColorTemperature) ?? $"0x{CurrentColorTemperature:X2}";
|
||||
|
||||
/// <summary>
|
||||
/// Maximum color temperature value
|
||||
/// Whether supports color temperature adjustment via VCP 0x14
|
||||
/// </summary>
|
||||
public int MaxColorTemperature { get; set; } = AppConstants.MonitorDefaults.MaxColorTemp;
|
||||
public bool SupportsColorTemperature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports color temperature adjustment
|
||||
/// Capabilities detection status: "available", "unavailable", or "unknown"
|
||||
/// </summary>
|
||||
public bool SupportsColorTemperature { get; set; } = true;
|
||||
public string CapabilitiesStatus { get; set; } = "unknown";
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports contrast adjustment
|
||||
|
||||
@@ -362,10 +362,9 @@ namespace PowerDisplay.Core
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor != null)
|
||||
{
|
||||
// Convert VCP value to approximate Kelvin temperature
|
||||
// This is a rough mapping - actual values depend on monitor implementation
|
||||
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
|
||||
monitor.CurrentColorTemperature = kelvin;
|
||||
// Store raw VCP 0x14 preset value (e.g., 0x05 for 6500K)
|
||||
// No Kelvin conversion - we use discrete presets
|
||||
monitor.CurrentColorTemperature = tempInfo.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// Get monitor by ID
|
||||
/// </summary>
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using PowerDisplay.Helpers;
|
||||
using static PowerDisplay.Native.NativeConstants;
|
||||
using static PowerDisplay.Native.NativeDelegates;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
@@ -29,14 +30,13 @@ namespace PowerDisplay.Native.DDC
|
||||
public partial class DdcCiController : IMonitorController, IDisposable
|
||||
{
|
||||
private readonly PhysicalMonitorHandleManager _handleManager = new();
|
||||
private readonly VcpCodeResolver _vcpResolver = new();
|
||||
private readonly MonitorDiscoveryHelper _discoveryHelper;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public DdcCiController()
|
||||
{
|
||||
_discoveryHelper = new MonitorDiscoveryHelper(_vcpResolver);
|
||||
_discoveryHelper = new MonitorDiscoveryHelper();
|
||||
}
|
||||
|
||||
public string Name => "DDC/CI Monitor Controller";
|
||||
@@ -63,7 +63,7 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor brightness
|
||||
/// Get monitor brightness using VCP code 0x10
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -73,29 +73,32 @@ namespace PowerDisplay.Native.DDC
|
||||
var physicalHandle = GetPhysicalHandle(monitor);
|
||||
if (physicalHandle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogDebug($"[{monitor.Id}] Invalid physical handle");
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
// First try high-level API
|
||||
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);
|
||||
}
|
||||
|
||||
// Try different VCP codes
|
||||
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out uint current, out uint max))
|
||||
// Try VCP code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out uint current, out uint max))
|
||||
{
|
||||
Logger.LogDebug($"[{monitor.Id}] Brightness via 0x10: {current}/{max}");
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
|
||||
Logger.LogWarning($"[{monitor.Id}] Failed to read brightness");
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor brightness
|
||||
/// Set monitor brightness using VCP code 0x10
|
||||
/// </summary>
|
||||
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);
|
||||
if (!currentInfo.IsValid)
|
||||
{
|
||||
Logger.LogWarning($"[{monitor.Id}] Cannot read current brightness");
|
||||
return MonitorOperationResult.Failure("Cannot read current brightness");
|
||||
}
|
||||
|
||||
@@ -123,21 +127,24 @@ namespace PowerDisplay.Native.DDC
|
||||
// First try high-level API
|
||||
if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue))
|
||||
{
|
||||
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via high-level API");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
// Try VCP codes
|
||||
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(physicalHandle, vcpCode.Value, targetValue))
|
||||
// Try VCP code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TrySetVCPFeature(physicalHandle, VcpCodeBrightness, targetValue))
|
||||
{
|
||||
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via 0x10");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{monitor.Id}] 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);
|
||||
|
||||
/// <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>
|
||||
public async Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -178,28 +186,32 @@ namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogDebug($"[{monitor.Id}] Invalid handle for color temperature read");
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
// Try different VCP codes for color temperature
|
||||
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(monitor.Handle, vcpCode.Value, out uint current, out uint max))
|
||||
// Try VCP code 0x14 (Select Color Preset)
|
||||
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, 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);
|
||||
}
|
||||
|
||||
Logger.LogWarning($"[{monitor.Id}] Failed to read color temperature (0x14 not supported)");
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor color temperature
|
||||
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
|
||||
/// </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)
|
||||
{
|
||||
colorTemperature = Math.Clamp(colorTemperature, 2000, 10000);
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
@@ -210,28 +222,34 @@ namespace PowerDisplay.Native.DDC
|
||||
|
||||
try
|
||||
{
|
||||
// Get current color temperature info to understand the range
|
||||
var currentInfo = _vcpResolver.GetCurrentColorTemperature(monitor.Handle);
|
||||
if (!currentInfo.IsValid)
|
||||
// Validate value is in supported list if capabilities available
|
||||
var capabilities = monitor.VcpCapabilitiesInfo;
|
||||
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
|
||||
uint targetValue = _vcpResolver.ConvertKelvinToVcpValue(colorTemperature, currentInfo);
|
||||
|
||||
// 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))
|
||||
// Set VCP 0x14 value
|
||||
var presetName = VcpValueNames.GetName(0x14, colorTemperature);
|
||||
if (DdcCiNative.TrySetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, (uint)colorTemperature))
|
||||
{
|
||||
Logger.LogInfo($"[{monitor.Id}] Set color temperature to 0x{colorTemperature:X2} ({presetName}) via 0x14");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{monitor.Id}] Exception setting color temperature: {ex.Message}");
|
||||
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
|
||||
}
|
||||
},
|
||||
@@ -607,7 +625,7 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get brightness information (with explicit handle)
|
||||
/// Get brightness information using VCP code 0x10 only
|
||||
/// </summary>
|
||||
private BrightnessInfo GetBrightnessInfo(Monitor monitor, IntPtr physicalHandle)
|
||||
{
|
||||
@@ -622,9 +640,8 @@ namespace PowerDisplay.Native.DDC
|
||||
return new BrightnessInfo((int)current, (int)min, (int)max);
|
||||
}
|
||||
|
||||
// Try VCP codes
|
||||
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out current, out max))
|
||||
// Try VCP code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out current, out max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
@@ -651,7 +668,6 @@ namespace PowerDisplay.Native.DDC
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_handleManager?.Dispose();
|
||||
_vcpResolver?.ClearCache();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,8 @@ namespace PowerDisplay.Native.DDC
|
||||
/// </summary>
|
||||
public class MonitorDiscoveryHelper
|
||||
{
|
||||
private readonly VcpCodeResolver _vcpResolver;
|
||||
|
||||
public MonitorDiscoveryHelper(VcpCodeResolver vcpResolver)
|
||||
public MonitorDiscoveryHelper()
|
||||
{
|
||||
_vcpResolver = vcpResolver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -183,34 +180,16 @@ namespace PowerDisplay.Native.DDC
|
||||
IsAvailable = true,
|
||||
Handle = physicalMonitor.HPhysicalMonitor,
|
||||
DeviceKey = deviceKey,
|
||||
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.DdcCi,
|
||||
Capabilities = MonitorCapabilities.DdcCi,
|
||||
ConnectionType = "External",
|
||||
CommunicationMethod = "DDC/CI",
|
||||
Manufacturer = ExtractManufacturer(name),
|
||||
CapabilitiesStatus = "unknown",
|
||||
};
|
||||
|
||||
// Check contrast support
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalMonitor.HPhysicalMonitor, VcpCodeContrast, out _, out _))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Note: Feature detection (brightness, contrast, color temp, volume) is now done
|
||||
// in MonitorManager after capabilities string is retrieved and parsed.
|
||||
// This ensures we rely on capabilities data rather than trial-and-error probing.
|
||||
return monitor;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -221,25 +200,21 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current brightness using VCP codes
|
||||
/// Get current brightness using VCP code 0x10 only
|
||||
/// </summary>
|
||||
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))
|
||||
{
|
||||
return new BrightnessInfo((int)current, (int)min, (int)max);
|
||||
}
|
||||
|
||||
// Try VCP codes
|
||||
byte[] vcpCodes = { VcpCodeBrightness, VcpCodeBacklightControl, VcpCodeBacklightLevelWhite, VcpCodeContrast };
|
||||
foreach (var code in vcpCodes)
|
||||
{
|
||||
if (DdcCiNative.TryGetVCPFeature(handle, code, out current, out max))
|
||||
// Try VCP code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TryGetVCPFeature(handle, VcpCodeBrightness, out current, out max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,60 +12,67 @@ namespace PowerDisplay.Native
|
||||
public static class NativeConstants
|
||||
{
|
||||
/// <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>
|
||||
public const byte VcpCodeBrightness = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Contrast
|
||||
/// VCP code: Contrast (0x12)
|
||||
/// Standard VESA MCCS contrast control.
|
||||
/// </summary>
|
||||
public const byte VcpCodeContrast = 0x12;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Backlight control (alternative brightness)
|
||||
/// VCP code: Backlight control (0x13)
|
||||
/// OBSOLETE: PowerDisplay now uses only VcpCodeBrightness (0x10).
|
||||
/// </summary>
|
||||
[Obsolete("Use VcpCodeBrightness (0x10) only. PowerDisplay no longer uses fallback codes.")]
|
||||
public const byte VcpCodeBacklightControl = 0x13;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: White backlight level
|
||||
/// VCP code: White backlight level (0x6B)
|
||||
/// OBSOLETE: PowerDisplay now uses only VcpCodeBrightness (0x10).
|
||||
/// </summary>
|
||||
[Obsolete("Use VcpCodeBrightness (0x10) only. PowerDisplay no longer uses fallback codes.")]
|
||||
public const byte VcpCodeBacklightLevelWhite = 0x6B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio speaker volume
|
||||
/// VCP code: Audio Speaker Volume (0x62)
|
||||
/// Standard VESA MCCS volume control for monitors with built-in speakers.
|
||||
/// </summary>
|
||||
public const byte VcpCodeVolume = 0x62;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio mute
|
||||
/// VCP code: Audio mute (0x8D)
|
||||
/// </summary>
|
||||
public const byte VcpCodeMute = 0x8D;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color Temperature Request (0x0C)
|
||||
/// Per MCCS v2.2a specification:
|
||||
/// - Used to SET and GET specific color temperature presets
|
||||
/// - Typically supports discrete values (e.g., 5000K, 6500K, 9300K)
|
||||
/// - Primary method for color temperature control
|
||||
/// OBSOLETE: Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.
|
||||
/// </summary>
|
||||
[Obsolete("Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.")]
|
||||
public const byte VcpCodeColorTemperature = 0x0C;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color Temperature Increment (0x0B)
|
||||
/// Per MCCS v2.2a specification:
|
||||
/// - Used for incremental color temperature adjustment
|
||||
/// - Typically supports continuous range (0-100 or custom range)
|
||||
/// - Fallback method when 0x0C is not supported
|
||||
/// OBSOLETE: Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.
|
||||
/// </summary>
|
||||
[Obsolete("Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.")]
|
||||
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Gamma correction (gamma adjustment)
|
||||
/// VCP code: Gamma correction (0x72)
|
||||
/// </summary>
|
||||
public const byte VcpCodeGamma = 0x72;
|
||||
|
||||
/// <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>
|
||||
public const byte VcpCodeSelectColorPreset = 0x14;
|
||||
|
||||
|
||||
@@ -195,50 +195,7 @@
|
||||
Text="{x:Bind Brightness, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Color Temperature Control -->
|
||||
<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=""
|
||||
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>
|
||||
<!-- Color Temperature Control removed - now in Settings UI -->
|
||||
|
||||
<!-- Contrast Control -->
|
||||
<Grid Height="40"
|
||||
|
||||
@@ -822,9 +822,8 @@ namespace PowerDisplay
|
||||
case "Brightness":
|
||||
monitorVm.Brightness = finalValue;
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
monitorVm.ColorTemperaturePercent = finalValue;
|
||||
break;
|
||||
|
||||
// ColorTemperature case removed - now controlled via Settings UI
|
||||
case "Contrast":
|
||||
monitorVm.ContrastPercent = finalValue;
|
||||
break;
|
||||
|
||||
@@ -498,17 +498,13 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}', skipping");
|
||||
}
|
||||
|
||||
// Color temperature must be valid and within range
|
||||
if (savedState.Value.ColorTemperature > 0 &&
|
||||
savedState.Value.ColorTemperature >= monitorVm.MinColorTemperature &&
|
||||
savedState.Value.ColorTemperature <= monitorVm.MaxColorTemperature)
|
||||
// Color temperature: VCP 0x14 preset value (discrete values, no range check needed)
|
||||
// Note: ColorTemperature is now read-only in flyout UI, controlled via Settings UI
|
||||
if (savedState.Value.ColorTemperature > 0)
|
||||
{
|
||||
// Validation will happen in Settings UI when applying preset values
|
||||
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
|
||||
if (monitorVm.ShowContrast &&
|
||||
@@ -649,7 +645,8 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
// Apply default values
|
||||
monitorVm.Brightness = 30;
|
||||
monitorVm.ColorTemperature = 6500;
|
||||
|
||||
// ColorTemperature is now read-only in flyout UI - controlled via Settings UI only
|
||||
monitorVm.Contrast = 50;
|
||||
monitorVm.Volume = 50;
|
||||
|
||||
@@ -729,6 +726,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
var result = new Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo
|
||||
{
|
||||
Code = $"0x{code:X2}",
|
||||
Title = $"{info.Name} (0x{code:X2})",
|
||||
};
|
||||
|
||||
@@ -744,6 +742,15 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
.ToList();
|
||||
result.Values = $"Values: {string.Join(", ", formattedValues)}";
|
||||
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
|
||||
{
|
||||
|
||||
@@ -28,12 +28,10 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
// Simple debouncers for each property (KISS principle - simpler than complex queue)
|
||||
private readonly SimpleDebouncer _brightnessDebouncer = new(300);
|
||||
private readonly SimpleDebouncer _colorTempDebouncer = new(300);
|
||||
private readonly SimpleDebouncer _contrastDebouncer = new(300);
|
||||
private readonly SimpleDebouncer _volumeDebouncer = new(300);
|
||||
|
||||
private int _brightness;
|
||||
private int _colorTemperature;
|
||||
private int _contrast;
|
||||
private int _volume;
|
||||
private bool _isAvailable;
|
||||
@@ -51,11 +49,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(ColorTemperature):
|
||||
_colorTemperature = value;
|
||||
OnPropertyChanged(nameof(ColorTemperature));
|
||||
OnPropertyChanged(nameof(ColorTemperaturePercent));
|
||||
break;
|
||||
// ColorTemperature removed - now controlled via Settings UI
|
||||
case nameof(Brightness):
|
||||
_brightness = value;
|
||||
OnPropertyChanged(nameof(Brightness));
|
||||
@@ -95,29 +89,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
_showContrast = monitor.SupportsContrast;
|
||||
_showVolume = monitor.SupportsVolume;
|
||||
|
||||
// Try to get current color temperature via DDC/CI, use default if failed
|
||||
try
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
// Color temperature initialization removed - now controlled via Settings UI
|
||||
// The Monitor.CurrentColorTemperature stores VCP 0x14 preset value (e.g., 0x05 for 6500K)
|
||||
// and will be initialized by MonitorManager based on capabilities
|
||||
|
||||
// Initialize basic properties from monitor
|
||||
_brightness = monitor.CurrentBrightness;
|
||||
@@ -152,10 +126,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
public int MaxBrightness => _monitor.MaxBrightness;
|
||||
|
||||
public int MinColorTemperature => _monitor.MinColorTemperature;
|
||||
|
||||
public int MaxColorTemperature => _monitor.MaxColorTemperature;
|
||||
|
||||
public int MinContrast => _monitor.MinContrast;
|
||||
|
||||
public int MaxContrast => _monitor.MaxContrast;
|
||||
@@ -238,41 +208,12 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorTemperature
|
||||
{
|
||||
get => _colorTemperature;
|
||||
set
|
||||
{
|
||||
if (_colorTemperature != value)
|
||||
{
|
||||
_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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Color temperature VCP preset value (from VCP code 0x14).
|
||||
/// Read-only in flyout UI - controlled via Settings UI.
|
||||
/// Returns the raw VCP value (e.g., 0x05 for 6500K).
|
||||
/// </summary>
|
||||
public int ColorTemperature => _monitor.CurrentColorTemperature;
|
||||
|
||||
public int Contrast
|
||||
{
|
||||
@@ -348,19 +289,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
});
|
||||
|
||||
public ICommand SetColorTemperatureCommand => new RelayCommand<int?>((temperature) =>
|
||||
{
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
// SetColorTemperatureCommand removed - now controlled via Settings UI
|
||||
public ICommand SetContrastCommand => new RelayCommand<int?>((contrast) =>
|
||||
{
|
||||
if (contrast.HasValue)
|
||||
@@ -378,16 +307,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
});
|
||||
|
||||
// Percentage-based properties for uniform slider behavior
|
||||
public int ColorTemperaturePercent
|
||||
{
|
||||
get => MapToPercent(_colorTemperature, MinColorTemperature, MaxColorTemperature);
|
||||
set
|
||||
{
|
||||
var actualValue = MapFromPercent(value, MinColorTemperature, MaxColorTemperature);
|
||||
ColorTemperature = actualValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ColorTemperaturePercent removed - now controlled via Settings UI
|
||||
public int ContrastPercent
|
||||
{
|
||||
get => MapToPercent(_contrast, MinContrast, MaxContrast);
|
||||
@@ -427,11 +347,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
// Notify percentage properties when actual values change
|
||||
if (propertyName == nameof(ColorTemperature))
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ColorTemperaturePercent)));
|
||||
}
|
||||
else if (propertyName == nameof(Contrast))
|
||||
if (propertyName == nameof(Contrast))
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContrastPercent)));
|
||||
}
|
||||
@@ -455,7 +371,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
// Dispose all debouncers
|
||||
_brightnessDebouncer?.Dispose();
|
||||
_colorTempDebouncer?.Dispose();
|
||||
_contrastDebouncer?.Dispose();
|
||||
_volumeDebouncer?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
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<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()
|
||||
{
|
||||
}
|
||||
@@ -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]
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
/// </summary>
|
||||
public class VcpCodeDisplayInfo
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
@@ -19,5 +22,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
[JsonPropertyName("hasValues")]
|
||||
public bool HasValues { get; set; }
|
||||
|
||||
[JsonPropertyName("valueList")]
|
||||
public System.Collections.Generic.List<VcpValueInfo> ValueList { get; set; } = new System.Collections.Generic.List<VcpValueInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
20
src/settings-ui/Settings.UI.Library/VcpValueInfo.cs
Normal file
20
src/settings-ui/Settings.UI.Library/VcpValueInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,23 @@
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="False">
|
||||
<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="" />
|
||||
</InfoBar.IconSource>
|
||||
</InfoBar>
|
||||
</tkcontrols:SettingsCard.Content>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Name">
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
@@ -102,17 +119,81 @@
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Brightness">
|
||||
<TextBlock Text="{x:Bind CurrentBrightness, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_ColorTemperature">
|
||||
<TextBlock Text="{x:Bind ColorTemperature, Mode=OneWay}" />
|
||||
<tkcontrols:SettingsCard
|
||||
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 x:Uid="PowerDisplay_Monitor_EnableColorTemperature">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch" IsOn="{x:Bind EnableColorTemperature, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsCard
|
||||
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 x:Uid="PowerDisplay_Monitor_EnableContrast">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableContrast_ToggleSwitch" IsOn="{x:Bind EnableContrast, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsCard
|
||||
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 x:Uid="PowerDisplay_Monitor_EnableVolume">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableVolume_ToggleSwitch" IsOn="{x:Bind EnableVolume, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsCard
|
||||
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 x:Uid="PowerDisplay_Monitor_HideMonitor">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" />
|
||||
|
||||
@@ -179,6 +179,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
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
|
||||
if (existingMonitors.TryGetValue(monitorKey, out var existingMonitor))
|
||||
{
|
||||
@@ -210,6 +216,110 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
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()
|
||||
{
|
||||
// Unsubscribe from monitor property changes
|
||||
|
||||
Reference in New Issue
Block a user