Add color temperature support to PowerDisplay

Enhanced PowerDisplay with support for applying color temperature settings.

- Added `APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT` and event handling logic.
- Introduced `ApplyColorTemperatureFromSettings` in `MainViewModel` for explicit hardware updates.
- Refactored `MonitorInfo` to dynamically compute and cache color temperature presets.
- Updated `ReloadMonitorsFromSettings` to preserve object references and improve UI responsiveness.
- Simplified UI bindings and removed redundant properties like `MonitorType`.
- Improved event handling in `dllmain.cpp` for the new color temperature action.
- Enhanced logging for better debugging and traceability.
- Updated JSON serialization context to include new types for color temperature.
- Removed unused code and improved documentation for maintainability.
This commit is contained in:
Yu Leng
2025-11-18 20:03:36 +08:00
parent 3f84ccc603
commit f10c9f49e9
12 changed files with 424 additions and 175 deletions

View File

@@ -137,6 +137,7 @@ namespace CommonSharedConstants
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
const wchar_t APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";

View File

@@ -31,6 +31,7 @@ namespace PowerDisplay
private const string TerminatePowerDisplayEvent = "Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
private const string RefreshMonitorsEvent = "Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
private const string SettingsUpdatedEvent = "Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
private const string ApplyColorTemperatureEvent = "Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
private Window? _mainWindow;
private int _powerToysRunnerPid;
@@ -156,6 +157,20 @@ namespace PowerDisplay
});
});
NativeEventWaiter.WaitForEventLoop(
ApplyColorTemperatureEvent,
() =>
{
Logger.LogInfo("Received apply color temperature event");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
mainWindow.ViewModel.ApplyColorTemperatureFromSettings();
}
});
});
// Monitor Runner process (backup exit mechanism)
if (_powerToysRunnerPid > 0)
{

View File

@@ -21,6 +21,18 @@ namespace PowerDisplay.Serialization
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(PowerDisplaySettings))]
// MonitorInfo and related types (Settings.UI.Library)
[JsonSerializable(typeof(MonitorInfo))]
[JsonSerializable(typeof(VcpCodeDisplayInfo))]
[JsonSerializable(typeof(VcpValueInfo))]
// Generic collection types
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<MonitorInfo>))]
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
[JsonSerializable(typeof(List<VcpValueInfo>))]
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,

View File

@@ -396,15 +396,20 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
{
try
{
// Read current color temperature from hardware
await _monitorManager.InitializeColorTemperatureAsync(monitorId);
// Update UI on dispatcher thread - get the monitor from manager
// Get the monitor and use the hardware value as-is
var monitor = _monitorManager.GetMonitor(monitorId);
if (monitor != null)
{
Logger.LogInfo($"[{monitorId}] Read color temperature from hardware: {monitor.CurrentColorTemperature}");
_dispatcherQueue.TryEnqueue(() =>
{
// Update color temperature without triggering hardware write
// Use the hardware value directly, even if not in the preset list
// This will also update monitor_state.json via MonitorStateManager
vm.UpdatePropertySilently(nameof(vm.ColorTemperature), monitor.CurrentColorTemperature);
});
}
@@ -424,10 +429,12 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
}
/// <summary>
/// Apply all settings changes from Settings UI (IPC event handler entry point)
/// Coordinates both UI configuration and hardware parameter updates
/// Apply settings changes from Settings UI (IPC event handler entry point)
/// Only applies UI configuration changes. Hardware parameter changes (e.g., color temperature)
/// should be triggered via custom actions to avoid unwanted side effects when non-hardware
/// settings (like RestoreSettingsOnStartup) are changed.
/// </summary>
public async void ApplySettingsFromUI()
public void ApplySettingsFromUI()
{
try
{
@@ -435,12 +442,10 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
// 1. Apply UI configuration changes (synchronous, lightweight)
// Apply UI configuration changes only (feature visibility toggles, etc.)
// Hardware parameters (brightness, color temperature) are applied via custom actions
ApplyUIConfiguration(settings);
// 2. Apply hardware parameter changes (asynchronous, may involve DDC/CI calls)
await ApplyHardwareParametersAsync(settings);
Logger.LogInfo("[Settings] Settings update complete");
}
catch (Exception ex)
@@ -475,6 +480,58 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
}
}
/// <summary>
/// Apply color temperature from settings (triggered by custom action from Settings UI)
/// This is called when user explicitly changes color temperature in Settings UI,
/// NOT when other settings change. Reads current settings and applies only color temperature.
/// </summary>
public async void ApplyColorTemperatureFromSettings()
{
try
{
Logger.LogInfo("[Settings] Processing color temperature update from Settings UI");
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
var updateTasks = new List<Task>();
foreach (var monitorVm in Monitors)
{
var hardwareId = monitorVm.HardwareId;
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m => m.HardwareId == hardwareId);
if (monitorSettings == null)
{
continue;
}
// Apply color temperature if changed
if (monitorSettings.ColorTemperature > 0 &&
monitorSettings.ColorTemperature != monitorVm.ColorTemperature)
{
Logger.LogInfo($"[Settings] Applying color temperature for {hardwareId}: 0x{monitorSettings.ColorTemperature:X2}");
var task = ApplyColorTemperatureAsync(monitorVm, monitorSettings.ColorTemperature);
updateTasks.Add(task);
}
}
// Wait for all updates to complete
if (updateTasks.Count > 0)
{
await Task.WhenAll(updateTasks);
Logger.LogInfo($"[Settings] Completed {updateTasks.Count} color temperature updates");
}
else
{
Logger.LogInfo("[Settings] No color temperature changes detected");
}
}
catch (Exception ex)
{
Logger.LogError($"[Settings] Failed to apply color temperature from settings: {ex.Message}");
}
}
/// <summary>
/// Apply hardware parameter changes (brightness, color temperature)
/// Asynchronous operation that communicates with monitor hardware via DDC/CI
@@ -810,19 +867,26 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
/// </summary>
private Microsoft.PowerToys.Settings.UI.Library.MonitorInfo CreateMonitorInfo(MonitorViewModel vm)
{
return new Microsoft.PowerToys.Settings.UI.Library.MonitorInfo(
var monitorInfo = new Microsoft.PowerToys.Settings.UI.Library.MonitorInfo(
name: vm.Name,
internalName: vm.Id,
hardwareId: vm.HardwareId,
communicationMethod: vm.CommunicationMethod,
monitorType: vm.IsInternal ? "Internal" : "External",
currentBrightness: vm.Brightness,
colorTemperature: vm.ColorTemperature)
{
CapabilitiesRaw = vm.CapabilitiesRaw,
VcpCodes = BuildVcpCodesList(vm),
VcpCodesFormatted = BuildFormattedVcpCodesList(vm),
// Infer support flags from VCP capabilities
// VCP 0x12 (18) = Contrast, 0x14 (20) = Color Temperature, 0x62 (98) = Volume
SupportsContrast = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x12) ?? false,
SupportsColorTemperature = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x14) ?? false,
SupportsVolume = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x62) ?? false,
};
return monitorInfo;
}
/// <summary>

View File

@@ -61,6 +61,7 @@ private:
HANDLE m_hTerminateEvent = nullptr;
HANDLE m_hRefreshEvent = nullptr;
HANDLE m_hSettingsUpdatedEvent = nullptr;
HANDLE m_hApplyColorTemperatureEvent = nullptr;
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
{
@@ -197,8 +198,9 @@ public:
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT);
m_hRefreshEvent = CreateDefaultEvent(CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT);
m_hSettingsUpdatedEvent = CreateDefaultEvent(CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT);
m_hApplyColorTemperatureEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT);
if (!m_hInvokeEvent || !m_hToggleEvent || !m_hTerminateEvent || !m_hRefreshEvent || !m_hSettingsUpdatedEvent)
if (!m_hInvokeEvent || !m_hToggleEvent || !m_hTerminateEvent || !m_hRefreshEvent || !m_hSettingsUpdatedEvent || !m_hApplyColorTemperatureEvent)
{
Logger::error(L"Failed to create one or more event handles");
}
@@ -237,6 +239,11 @@ public:
CloseHandle(m_hSettingsUpdatedEvent);
m_hSettingsUpdatedEvent = nullptr;
}
if (m_hApplyColorTemperatureEvent)
{
CloseHandle(m_hApplyColorTemperatureEvent);
m_hApplyColorTemperatureEvent = nullptr;
}
}
virtual void destroy() override
@@ -307,6 +314,19 @@ public:
Logger::warn(L"Refresh event handle is null");
}
}
else if (action_object.get_name() == L"ApplyColorTemperature")
{
Logger::trace(L"ApplyColorTemperature action received");
if (m_hApplyColorTemperatureEvent)
{
Logger::trace(L"Signaling apply color temperature event");
SetEvent(m_hApplyColorTemperatureEvent);
}
else
{
Logger::warn(L"Apply color temperature event handle is null");
}
}
}
catch (std::exception&)
{

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library
@@ -16,7 +17,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
private string _internalName = string.Empty;
private string _hardwareId = string.Empty;
private string _communicationMethod = string.Empty;
private string _monitorType = string.Empty;
private int _currentBrightness;
private int _colorTemperature = 6500;
private bool _isHidden;
@@ -33,8 +33,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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>();
// Cached color temperature presets (computed from VcpCodesFormatted)
private ObservableCollection<ColorPresetItem> _availableColorPresetsCache;
public MonitorInfo()
{
@@ -47,13 +47,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
CommunicationMethod = communicationMethod;
}
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, string monitorType, int currentBrightness, int colorTemperature)
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, int currentBrightness, int colorTemperature)
{
Name = name;
InternalName = internalName;
HardwareId = hardwareId;
CommunicationMethod = communicationMethod;
MonitorType = monitorType;
CurrentBrightness = currentBrightness;
ColorTemperature = colorTemperature;
}
@@ -114,20 +113,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
[JsonPropertyName("monitorType")]
public string MonitorType
{
get => _monitorType;
set
{
if (_monitorType != value)
{
_monitorType = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("currentBrightness")]
public int CurrentBrightness
{
@@ -152,6 +137,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
_colorTemperature = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Update display list when current value changes
}
}
}
@@ -237,7 +223,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (_vcpCodesFormatted != value)
{
_vcpCodesFormatted = value ?? new List<VcpCodeDisplayInfo>();
_availableColorPresetsCache = null; // Clear cache when VCP codes change
OnPropertyChanged();
OnPropertyChanged(nameof(AvailableColorPresets));
}
}
}
@@ -299,8 +287,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (_supportsColorTemperature != value)
{
_supportsColorTemperature = value;
_availableColorPresetsCache = null; // Clear cache when support status changes
OnPropertyChanged();
OnPropertyChanged(nameof(ColorTemperatureTooltip));
OnPropertyChanged(nameof(AvailableColorPresets)); // Refresh computed property
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Refresh display list
}
}
}
@@ -335,23 +326,175 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
[JsonPropertyName("availableColorPresets")]
/// <summary>
/// Available color temperature presets computed from VcpCodesFormatted (VCP code 0x14).
/// This is a computed property that parses the VCP capabilities data on-demand.
/// </summary>
[JsonIgnore]
public ObservableCollection<ColorPresetItem> AvailableColorPresets
{
get => _availableColorPresets;
set
get
{
if (_availableColorPresets != value)
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] GET called for monitor '{_name}'");
// Return cached value if available
if (_availableColorPresetsCache != null)
{
_availableColorPresets = value ?? new ObservableCollection<ColorPresetItem>();
OnPropertyChanged();
OnPropertyChanged(nameof(HasColorPresets));
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] Cache HIT - returning {_availableColorPresetsCache.Count} items");
return _availableColorPresetsCache;
}
Logger.LogInfo("[MonitorInfo.AvailableColorPresets] Cache MISS - computing from VcpCodesFormatted");
// Compute from VcpCodesFormatted
_availableColorPresetsCache = ComputeAvailableColorPresets();
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] Computed {_availableColorPresetsCache.Count} items");
return _availableColorPresetsCache;
}
}
/// <summary>
/// Compute available color presets from VcpCodesFormatted (VCP code 0x14)
/// </summary>
private ObservableCollection<ColorPresetItem> ComputeAvailableColorPresets()
{
Logger.LogInfo($"[ComputeAvailableColorPresets] START for monitor '{_name}'");
Logger.LogInfo($" - SupportsColorTemperature: {_supportsColorTemperature}");
Logger.LogInfo($" - VcpCodesFormatted: {(_vcpCodesFormatted == null ? "NULL" : $"{_vcpCodesFormatted.Count} items")}");
// Check if color temperature is supported
if (!_supportsColorTemperature || _vcpCodesFormatted == null)
{
Logger.LogWarning($"[ComputeAvailableColorPresets] Color temperature not supported or no VCP codes - returning empty");
return new ObservableCollection<ColorPresetItem>();
}
// Find VCP code 0x14 (Color Temperature / Select Color Preset)
var colorTempVcp = _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;
});
Logger.LogInfo($"[ComputeAvailableColorPresets] VCP 0x14 found: {colorTempVcp != null}");
if (colorTempVcp != null)
{
Logger.LogInfo($" - ValueList: {(colorTempVcp.ValueList == null ? "NULL" : $"{colorTempVcp.ValueList.Count} items")}");
}
// No VCP 0x14 or no values
if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
{
Logger.LogWarning($"[ComputeAvailableColorPresets] No VCP 0x14 or empty ValueList - returning empty");
return new ObservableCollection<ColorPresetItem>();
}
// Build preset list from supported values
var presetList = new List<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 = FormatColorTemperatureDisplayName(valueInfo.Name, vcpValue);
presetList.Add(new ColorPresetItem(vcpValue, displayName));
Logger.LogDebug($"[ComputeAvailableColorPresets] Added: {displayName}");
}
}
// Sort by VCP value for consistent ordering
presetList = presetList.OrderBy(p => p.VcpValue).ToList();
Logger.LogInfo($"[ComputeAvailableColorPresets] COMPLETE - returning {presetList.Count} items");
Logger.LogInfo($"[ComputeAvailableColorPresets] Current ColorTemperature value: {_colorTemperature}");
return new ObservableCollection<ColorPresetItem>(presetList);
}
/// <summary>
/// Format color temperature display name
/// </summary>
private string FormatColorTemperatureDisplayName(string name, int vcpValue)
{
var hexValue = $"0x{vcpValue:X2}";
// Check if name is undefined (null or empty)
if (string.IsNullOrEmpty(name))
{
return $"Manufacturer Defined ({hexValue})";
}
// For predefined names, append the hex value in parentheses
return $"{name} ({hexValue})";
}
/// <summary>
/// Color presets for display in ComboBox, includes current value if not in preset list
/// </summary>
[JsonIgnore]
public bool HasColorPresets => _availableColorPresets != null && _availableColorPresets.Count > 0;
public ObservableCollection<ColorPresetItem> ColorPresetsForDisplay
{
get
{
var presets = AvailableColorPresets;
if (presets == null || presets.Count == 0)
{
return new ObservableCollection<ColorPresetItem>();
}
// Check if current value is in the preset list
var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperature);
if (currentValueInList)
{
// Current value is in the list, return as-is
return presets;
}
// Current value is not in the preset list - add it at the beginning
var displayList = new List<ColorPresetItem>();
// Add current value with "Custom" indicator
var currentValueName = GetColorTemperatureName(_colorTemperature);
var displayName = string.IsNullOrEmpty(currentValueName)
? $"Custom (0x{_colorTemperature:X2})"
: $"{currentValueName} (0x{_colorTemperature:X2}) - Custom";
displayList.Add(new ColorPresetItem(_colorTemperature, displayName));
// Add all supported presets
displayList.AddRange(presets);
return new ObservableCollection<ColorPresetItem>(displayList);
}
}
/// <summary>
/// Get the name for a color temperature value from standard VCP naming
/// </summary>
private string GetColorTemperatureName(int vcpValue)
{
return vcpValue switch
{
0x04 => "5000K",
0x05 => "6500K",
0x06 => "7500K",
0x08 => "9300K",
0x09 => "10000K",
0x0A => "11500K",
0x0B => "User 1",
0x0C => "User 2",
0x0D => "User 3",
_ => null,
};
}
[JsonIgnore]
public bool HasColorPresets => AvailableColorPresets != null && AvailableColorPresets.Count > 0;
[JsonIgnore]
public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
@@ -407,12 +550,35 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
/// <summary>
/// Refreshes the ColorTemperature property binding to force UI re-evaluation.
/// Called after AvailableColorPresets is populated to sync ComboBox selection.
/// Update this monitor's properties from another MonitorInfo instance.
/// This preserves the object reference while updating all properties.
/// </summary>
public void RefreshColorTemperatureBinding()
/// <param name="other">The source MonitorInfo to copy properties from</param>
public void UpdateFrom(MonitorInfo other)
{
OnPropertyChanged(nameof(ColorTemperature));
if (other == null)
{
return;
}
// Update all properties that can change
Name = other.Name;
InternalName = other.InternalName;
HardwareId = other.HardwareId;
CommunicationMethod = other.CommunicationMethod;
CurrentBrightness = other.CurrentBrightness;
ColorTemperature = other.ColorTemperature;
IsHidden = other.IsHidden;
EnableContrast = other.EnableContrast;
EnableVolume = other.EnableVolume;
CapabilitiesRaw = other.CapabilitiesRaw;
VcpCodes = other.VcpCodes;
VcpCodesFormatted = other.VcpCodesFormatted;
SupportsBrightness = other.SupportsBrightness;
SupportsContrast = other.SupportsContrast;
SupportsColorTemperature = other.SupportsColorTemperature;
SupportsVolume = other.SupportsVolume;
CapabilitiesStatus = other.CapabilitiesStatus;
}
/// <summary>

View File

@@ -24,9 +24,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("communicationMethod")]
public string CommunicationMethod { get; set; } = string.Empty;
[JsonPropertyName("monitorType")]
public string MonitorType { get; set; } = string.Empty;
[JsonPropertyName("currentBrightness")]
public int CurrentBrightness { get; set; }

View File

@@ -16,7 +16,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public PowerDisplayProperties()
{
ActivationShortcut = DefaultActivationShortcut;
LaunchAtStartup = false;
BrightnessUpdateRate = "1s";
Monitors = new List<MonitorInfo>();
RestoreSettingsOnStartup = true;
@@ -28,9 +27,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }
[JsonPropertyName("launch_at_startup")]
public bool LaunchAtStartup { get; set; }
[JsonPropertyName("brightness_update_rate")]
public string BrightnessUpdateRate { get; set; }

View File

@@ -2,6 +2,7 @@
// 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.Collections.Generic;
using System.Text.Json.Serialization;
using SettingsUILibrary = Settings.UI.Library;
using SettingsUILibraryHelpers = Settings.UI.Library.Helpers;
@@ -134,6 +135,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(MonitorInfo))]
[JsonSerializable(typeof(MonitorInfoData))]
[JsonSerializable(typeof(PowerDisplayActionMessage))]
[JsonSerializable(typeof(PowerDisplayActionMessage.ActionData))]
[JsonSerializable(typeof(PowerDisplayActionMessage.PowerDisplayAction))]
[JsonSerializable(typeof(VcpCodeDisplayInfo))]
[JsonSerializable(typeof(VcpValueInfo))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<MonitorInfo>))]
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
[JsonSerializable(typeof(List<VcpValueInfo>))]
[JsonSerializable(typeof(SettingsUILibraryHelpers.SearchLocation))]
[JsonSerializable(typeof(SndLightSwitchSettings))]

View File

@@ -18,10 +18,10 @@
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_Enable_PowerDisplay"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
<controls:SettingsGroup x:Uid="Shortcut" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
<controls:SettingsGroup x:Uid="Shortcut" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_ActivationShortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
@@ -29,7 +29,7 @@
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Configuration_GroupSettings" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
<controls:SettingsGroup x:Uid="PowerDisplay_Configuration_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_LaunchButtonControl"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
@@ -37,12 +37,6 @@
HeaderIcon="{ui:FontIcon Glyph=&#xE770;}"
IsClickEnabled="True" />
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_LaunchAtStartup"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B5;}">
<ToggleSwitch x:Uid="PowerDisplay_LaunchAtStartup_ToggleSwitch" IsOn="{x:Bind ViewModel.IsLaunchAtStartupEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_RestoreSettingsOnStartup"
HeaderIcon="{ui:FontIcon Glyph=&#xE7B8;}">
@@ -60,12 +54,12 @@
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
<controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<!-- Empty state hint -->
<InfoBar
x:Uid="PowerDisplay_NoMonitorsDetected"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}"
IsOpen="{x:Bind ViewModel.IsEnabled, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
Severity="Informational">
<InfoBar.IconSource>
@@ -110,15 +104,9 @@
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_InternalName">
<TextBlock Text="{x:Bind InternalName, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Type">
<TextBlock Text="{x:Bind MonitorType, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_CommunicationMethod">
<TextBlock Text="{x:Bind CommunicationMethod, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Brightness">
<TextBlock Text="{x:Bind CurrentBrightness, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_Monitor_ColorTemperature"
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
@@ -143,14 +131,14 @@
<ComboBox
x:Name="ColorTemperatureComboBox"
MinWidth="200"
ItemsSource="{x:Bind AvailableColorPresets, Mode=OneWay}"
SelectedValue="{x:Bind ColorTemperature, Mode=OneWay}"
ItemsSource="{Binding ColorPresetsForDisplay, Mode=OneWay}"
SelectedValue="{Binding ColorTemperature, Mode=TwoWay}"
SelectedValuePath="VcpValue"
DisplayMemberPath="DisplayName"
PlaceholderText="Not available"
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}"
IsEnabled="{Binding SupportsColorTemperature, Mode=OneWay}"
SelectionChanged="ColorTemperatureComboBox_SelectionChanged"
Tag="{x:Bind}">
Tag="{Binding}">
<ToolTipService.ToolTip>
<TextBlock Text="Changing this setting requires confirmation" />
</ToolTipService.ToolTip>

View File

@@ -135,8 +135,14 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (result == ContentDialogResult.Primary)
{
// User confirmed, apply the change
// Setting the property will trigger save to settings file via OnPropertyChanged
monitor.ColorTemperature = newValue.Value;
_previousColorTemperatureValues[monitor.HardwareId] = newValue.Value;
// Trigger custom action to apply color temperature to hardware
// This is separate from the settings save to avoid unwanted hardware updates
// when other settings (like RestoreSettingsOnStartup) change
ViewModel.TriggerApplyColorTemperature();
}
else
{

View File

@@ -26,8 +26,10 @@ using PowerToys.Interop;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class PowerDisplayViewModel : Observable
public partial class PowerDisplayViewModel : PageViewModelBase
{
protected override string ModuleName => PowerDisplaySettings.ModuleName;
private GeneralSettings GeneralSettingsConfig { get; set; }
private ISettingsUtils SettingsUtils { get; set; }
@@ -49,10 +51,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Initialize monitors collection using property setter for proper subscription setup
// Parse capabilities for each loaded monitor to ensure UI displays correctly
var loadedMonitors = _settings.Properties.Monitors;
Logger.LogInfo($"[Constructor] Initializing with {loadedMonitors.Count} monitors from settings");
foreach (var monitor in loadedMonitors)
{
// Parse capabilities to determine feature support
ParseFeatureSupportFromCapabilities(monitor);
PopulateColorPresetsForMonitor(monitor);
}
Monitors = new ObservableCollection<MonitorInfo>(loadedMonitors);
@@ -72,18 +77,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private void InitializeEnabledValue()
{
_isPowerDisplayEnabled = GeneralSettingsConfig.Enabled.PowerDisplay;
_isEnabled = GeneralSettingsConfig.Enabled.PowerDisplay;
}
public bool IsPowerDisplayEnabled
public bool IsEnabled
{
get => _isPowerDisplayEnabled;
get => _isEnabled;
set
{
if (_isPowerDisplayEnabled != value)
if (_isEnabled != value)
{
_isPowerDisplayEnabled = value;
OnPropertyChanged(nameof(IsPowerDisplayEnabled));
_isEnabled = value;
OnPropertyChanged(nameof(IsEnabled));
GeneralSettingsConfig.Enabled.PowerDisplay = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
@@ -92,12 +97,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool IsLaunchAtStartupEnabled
{
get => _settings.Properties.LaunchAtStartup;
set => SetSettingsProperty(_settings.Properties.LaunchAtStartup, value, v => _settings.Properties.LaunchAtStartup = v);
}
public bool RestoreSettingsOnStartup
{
get => _settings.Properties.RestoreSettingsOnStartup;
@@ -194,9 +193,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// 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))
{
@@ -276,88 +272,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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))
{
// Format display name for Settings UI
var displayName = FormatColorTemperatureDisplayName(valueInfo.Name, vcpValue);
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
monitor.AvailableColorPresets = new ObservableCollection<MonitorInfo.ColorPresetItem>(presetList);
// Refresh ColorTemperature binding to force ComboBox to re-evaluate SelectedValue
// and match it against the newly populated AvailableColorPresets
monitor.RefreshColorTemperatureBinding();
}
/// <summary>
/// Format color temperature display name for Settings UI
/// Examples:
/// - Undefined values: "Manufacturer Defined (0x05)"
/// - Predefined values: "6500K (0x05)", "sRGB (0x01)"
/// </summary>
private string FormatColorTemperatureDisplayName(string name, int vcpValue)
{
var hexValue = $"0x{vcpValue:X2}";
// Check if name is undefined (null or empty)
// GetName now returns null for unknown values instead of hex string
if (string.IsNullOrEmpty(name))
{
return $"Manufacturer Defined ({hexValue})";
}
// For predefined names, append the hex value in parentheses
// Examples: "6500K (0x05)", "sRGB (0x01)"
return $"{name} ({hexValue})";
}
public void Dispose()
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Base class PageViewModelBase.Dispose() handles GC.SuppressFinalize")]
public override void Dispose()
{
// Unsubscribe from monitor property changes
UnsubscribeFromItemPropertyChanged(_monitors);
@@ -367,6 +283,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
_monitors.CollectionChanged -= Monitors_CollectionChanged;
}
base.Dispose();
}
/// <summary>
@@ -431,6 +349,27 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
SendConfigMSG(JsonSerializer.Serialize(actionMessage));
}
/// <summary>
/// Trigger PowerDisplay.exe to apply color temperature from settings file
/// Called after user confirms color temperature change in Settings UI
/// </summary>
public void TriggerApplyColorTemperature()
{
var actionMessage = new PowerDisplayActionMessage
{
Action = new PowerDisplayActionMessage.ActionData
{
PowerDisplay = new PowerDisplayActionMessage.PowerDisplayAction
{
ActionName = "ApplyColorTemperature",
Value = string.Empty,
},
},
};
SendConfigMSG(JsonSerializer.Serialize(actionMessage));
}
/// <summary>
/// Reload monitor list from settings file (called when PowerDisplay.exe signals monitor changes)
/// </summary>
@@ -444,16 +383,52 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var updatedSettings = SettingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
var updatedMonitors = updatedSettings.Properties.Monitors;
Logger.LogInfo($"[ReloadMonitors] Loaded {updatedMonitors.Count} monitors from settings");
// Parse capabilities for each monitor
foreach (var monitor in updatedMonitors)
{
ParseFeatureSupportFromCapabilities(monitor);
PopulateColorPresetsForMonitor(monitor);
}
// Update the monitors collection
// This will trigger UI update through property change notification
Monitors = new ObservableCollection<MonitorInfo>(updatedMonitors);
// Update existing MonitorInfo objects instead of replacing the collection
// This preserves XAML x:Bind bindings which reference specific object instances
if (Monitors == null)
{
// First time initialization - create new collection
Monitors = new ObservableCollection<MonitorInfo>(updatedMonitors);
}
else
{
// Create a dictionary for quick lookup by InternalName
var updatedMonitorsDict = updatedMonitors.ToDictionary(m => m.InternalName, m => m);
// Update existing monitors or remove ones that no longer exist
for (int i = Monitors.Count - 1; i >= 0; i--)
{
var existingMonitor = Monitors[i];
if (updatedMonitorsDict.TryGetValue(existingMonitor.InternalName, out var updatedMonitor))
{
// Monitor still exists - update its properties in place
Logger.LogInfo($"[ReloadMonitors] Updating existing monitor: {existingMonitor.InternalName}");
existingMonitor.UpdateFrom(updatedMonitor);
updatedMonitorsDict.Remove(existingMonitor.InternalName);
}
else
{
// Monitor no longer exists - remove from collection
Logger.LogInfo($"[ReloadMonitors] Removing monitor: {existingMonitor.InternalName}");
Monitors.RemoveAt(i);
}
}
// Add any new monitors that weren't in the existing collection
foreach (var newMonitor in updatedMonitorsDict.Values)
{
Logger.LogInfo($"[ReloadMonitors] Adding new monitor: {newMonitor.InternalName}");
Monitors.Add(newMonitor);
}
}
// Update internal settings reference
_settings.Properties.Monitors = updatedMonitors;
@@ -468,7 +443,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private Func<string, int> SendConfigMSG { get; }
private bool _isPowerDisplayEnabled;
private bool _isEnabled;
private PowerDisplaySettings _settings;
private ObservableCollection<MonitorInfo> _monitors;
private bool _hasMonitors;
@@ -476,7 +451,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public void RefreshEnabledState()
{
InitializeEnabledValue();
OnPropertyChanged(nameof(IsPowerDisplayEnabled));
OnPropertyChanged(nameof(IsEnabled));
}
private bool SetSettingsProperty<T>(T currentValue, T newValue, Action<T> setter, [CallerMemberName] string propertyName = null)