diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 4707a1adb7..f4e4106877 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -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"; diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs index 92e83b15f8..9a17f1985c 100644 --- a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs +++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs @@ -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) { diff --git a/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs index f4ab3ba5b2..4ee496d2dd 100644 --- a/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs +++ b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs @@ -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))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSourceGenerationOptions( WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs index 648cfc56cd..1ed5649b66 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs @@ -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 } /// - /// 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. /// - public async void ApplySettingsFromUI() + public void ApplySettingsFromUI() { try { @@ -435,12 +442,10 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable var settings = _settingsUtils.GetSettingsOrDefault("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 } } + /// + /// 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. + /// + public async void ApplyColorTemperatureFromSettings() + { + try + { + Logger.LogInfo("[Settings] Processing color temperature update from Settings UI"); + + var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay"); + var updateTasks = new List(); + + 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}"); + } + } + /// /// 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 /// 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; } /// diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp index f17697f1e3..0b699d9e8b 100644 --- a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp +++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp @@ -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&) { diff --git a/src/settings-ui/Settings.UI.Library/MonitorInfo.cs b/src/settings-ui/Settings.UI.Library/MonitorInfo.cs index 8ec7d8cf25..74db73752d 100644 --- a/src/settings-ui/Settings.UI.Library/MonitorInfo.cs +++ b/src/settings-ui/Settings.UI.Library/MonitorInfo.cs @@ -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 _availableColorPresets = new ObservableCollection(); + // Cached color temperature presets (computed from VcpCodesFormatted) + private ObservableCollection _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(); + _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")] + /// + /// Available color temperature presets computed from VcpCodesFormatted (VCP code 0x14). + /// This is a computed property that parses the VCP capabilities data on-demand. + /// + [JsonIgnore] public ObservableCollection 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(); - 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; } } + /// + /// Compute available color presets from VcpCodesFormatted (VCP code 0x14) + /// + private ObservableCollection 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(); + } + + // 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(); + } + + // Build preset list from supported values + var presetList = new List(); + 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(presetList); + } + + /// + /// Format color temperature display name + /// + 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})"; + } + + /// + /// Color presets for display in ComboBox, includes current value if not in preset list + /// [JsonIgnore] - public bool HasColorPresets => _availableColorPresets != null && _availableColorPresets.Count > 0; + public ObservableCollection ColorPresetsForDisplay + { + get + { + var presets = AvailableColorPresets; + if (presets == null || presets.Count == 0) + { + return new ObservableCollection(); + } + + // 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(); + + // 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(displayList); + } + } + + /// + /// Get the name for a color temperature value from standard VCP naming + /// + 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 } /// - /// 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. /// - public void RefreshColorTemperatureBinding() + /// The source MonitorInfo to copy properties from + 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; } /// diff --git a/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs b/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs index 649f68cec9..81a663ddf9 100644 --- a/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs +++ b/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs @@ -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; } diff --git a/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs b/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs index 33ce38c056..64bc7473af 100644 --- a/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs @@ -16,7 +16,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library public PowerDisplayProperties() { ActivationShortcut = DefaultActivationShortcut; - LaunchAtStartup = false; BrightnessUpdateRate = "1s"; Monitors = new List(); 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; } diff --git a/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs index 612b48ef8a..7a718086e1 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs @@ -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))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] [JsonSerializable(typeof(SettingsUILibraryHelpers.SearchLocation))] [JsonSerializable(typeof(SndLightSwitchSettings))] diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml index f676acb787..27ab8d5757 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml @@ -18,10 +18,10 @@ - + - + @@ -29,7 +29,7 @@ - + - - - - @@ -60,12 +54,12 @@ - + @@ -110,15 +104,9 @@ - - - - - - @@ -143,14 +131,14 @@ + Tag="{Binding}"> diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs index 10d506e36f..f84bd85711 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs @@ -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 { diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs index dc000d6870..e3c3f793a5 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs @@ -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(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); } - /// - /// Populate color temperature presets for a monitor from VcpCodesFormatted - /// Builds the ComboBox items from VCP code 0x14 supported values - /// - 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(); - 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(); - return; - } - - // Build preset list from supported values - var presetList = new List(); - 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(presetList); - - // Refresh ColorTemperature binding to force ComboBox to re-evaluate SelectedValue - // and match it against the newly populated AvailableColorPresets - monitor.RefreshColorTemperatureBinding(); - } - - /// - /// Format color temperature display name for Settings UI - /// Examples: - /// - Undefined values: "Manufacturer Defined (0x05)" - /// - Predefined values: "6500K (0x05)", "sRGB (0x01)" - /// - 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(); } /// @@ -431,6 +349,27 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels SendConfigMSG(JsonSerializer.Serialize(actionMessage)); } + /// + /// Trigger PowerDisplay.exe to apply color temperature from settings file + /// Called after user confirms color temperature change in Settings UI + /// + public void TriggerApplyColorTemperature() + { + var actionMessage = new PowerDisplayActionMessage + { + Action = new PowerDisplayActionMessage.ActionData + { + PowerDisplay = new PowerDisplayActionMessage.PowerDisplayAction + { + ActionName = "ApplyColorTemperature", + Value = string.Empty, + }, + }, + }; + + SendConfigMSG(JsonSerializer.Serialize(actionMessage)); + } + /// /// Reload monitor list from settings file (called when PowerDisplay.exe signals monitor changes) /// @@ -444,16 +383,52 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels var updatedSettings = SettingsUtils.GetSettingsOrDefault(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(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(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 SendConfigMSG { get; } - private bool _isPowerDisplayEnabled; + private bool _isEnabled; private PowerDisplaySettings _settings; private ObservableCollection _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 currentValue, T newValue, Action setter, [CallerMemberName] string propertyName = null)