diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Interfaces/IMonitorController.cs b/src/modules/powerdisplay/PowerDisplay/Core/Interfaces/IMonitorController.cs index 6accebd953..e331ffc920 100644 --- a/src/modules/powerdisplay/PowerDisplay/Core/Interfaces/IMonitorController.cs +++ b/src/modules/powerdisplay/PowerDisplay/Core/Interfaces/IMonitorController.cs @@ -21,11 +21,6 @@ namespace PowerDisplay.Core.Interfaces /// string Name { get; } - /// - /// Supported monitor type - /// - MonitorType SupportedType { get; } - /// /// Checks whether the specified monitor can be controlled /// diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs b/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs index 7b34939361..b87f7d6ec6 100644 --- a/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs +++ b/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs @@ -35,11 +35,6 @@ namespace PowerDisplay.Core.Models /// public string Name { get; set; } = string.Empty; - /// - /// Monitor type - /// - public MonitorType Type { get; set; } = MonitorType.Unknown; - /// /// Current brightness (0-100) /// @@ -87,10 +82,10 @@ namespace PowerDisplay.Core.Models } /// - /// Human-readable color temperature preset name (e.g., "6500K", "sRGB") + /// Human-readable color temperature preset name (e.g., "6500K (0x05)", "sRGB (0x01)") /// public string ColorTemperaturePresetName => - VcpValueNames.GetName(0x14, CurrentColorTemperature) ?? $"0x{CurrentColorTemperature:X2}"; + VcpValueNames.GetFormattedName(0x14, CurrentColorTemperature); /// /// Whether supports color temperature adjustment via VCP 0x14 @@ -244,7 +239,7 @@ namespace PowerDisplay.Core.Models public override string ToString() { - return $"{Name} ({Type}) - {CurrentBrightness}%"; + return $"{Name} ({CommunicationMethod}) - {CurrentBrightness}%"; } /// diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Models/MonitorType.cs b/src/modules/powerdisplay/PowerDisplay/Core/Models/MonitorType.cs deleted file mode 100644 index 007ef275ec..0000000000 --- a/src/modules/powerdisplay/PowerDisplay/Core/Models/MonitorType.cs +++ /dev/null @@ -1,32 +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. - -namespace PowerDisplay.Core.Models -{ - /// - /// Monitor type enumeration - /// - public enum MonitorType - { - /// - /// Unknown type - /// - Unknown, - - /// - /// Internal display (laptop screen, controlled via WMI) - /// - Internal, - - /// - /// External display (controlled via DDC/CI) - /// - External, - - /// - /// HDR display (controlled via Display Config API) - /// - HDR, - } -} diff --git a/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs b/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs index ae743bae5c..cb2db95f46 100644 --- a/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs +++ b/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs @@ -11,6 +11,7 @@ using ManagedCommon; using PowerDisplay.Core.Interfaces; using PowerDisplay.Core.Models; using PowerDisplay.Core.Utils; +using PowerDisplay.Native; using PowerDisplay.Native.DDC; using PowerDisplay.Native.WMI; using Monitor = PowerDisplay.Core.Models.Monitor; @@ -127,8 +128,9 @@ namespace PowerDisplay.Core Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}"); } - // Get capabilities for DDC/CI monitors (External type) - if (monitor.Type == MonitorType.External && controller.SupportedType == MonitorType.External) + // Get capabilities for DDC/CI monitors + // Check by CommunicationMethod instead of Type + if (monitor.CommunicationMethod?.Contains("DDC", StringComparison.OrdinalIgnoreCase) == true) { try { @@ -143,6 +145,12 @@ namespace PowerDisplay.Core monitor.VcpCapabilitiesInfo = Utils.VcpCapabilitiesParser.Parse(capsString); Logger.LogInfo($"Successfully parsed capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes"); + + // Update capability flags based on parsed VCP codes + if (monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0) + { + UpdateMonitorCapabilitiesFromVcp(monitor); + } } else { @@ -239,7 +247,7 @@ namespace PowerDisplay.Core var controller = GetControllerForMonitor(monitor); if (controller == null) { - Logger.LogError($"No controller available for monitor {monitorId}, Type={monitor.Type}"); + Logger.LogError($"No controller available for monitor {monitorId}"); return MonitorOperationResult.Failure("No controller available for this monitor"); } @@ -387,7 +395,8 @@ namespace PowerDisplay.Core /// private IMonitorController? GetControllerForMonitor(Monitor monitor) { - return _controllers.FirstOrDefault(c => c.SupportedType == monitor.Type); + // WMI monitors use WmiController, DDC/CI monitors use DdcCiController + return _controllers.FirstOrDefault(c => c.CanControlMonitorAsync(monitor).GetAwaiter().GetResult()); } /// @@ -411,7 +420,7 @@ namespace PowerDisplay.Core var controller = GetControllerForMonitor(monitor); if (controller == null) { - Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}, Type={monitor.Type}"); + Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}"); return MonitorOperationResult.Failure("No controller available for this monitor"); } @@ -439,6 +448,41 @@ namespace PowerDisplay.Core } } + /// + /// Update monitor capability flags based on parsed VCP capabilities + /// + private void UpdateMonitorCapabilitiesFromVcp(Monitor monitor) + { + var vcpCaps = monitor.VcpCapabilitiesInfo; + if (vcpCaps == null) + { + return; + } + + // Check for Contrast support (VCP 0x12) + if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeContrast)) + { + monitor.Capabilities |= MonitorCapabilities.Contrast; + Logger.LogDebug($"[{monitor.Id}] Contrast support detected via VCP 0x12"); + } + + // Check for Volume support (VCP 0x62) + if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeVolume)) + { + monitor.Capabilities |= MonitorCapabilities.Volume; + Logger.LogDebug($"[{monitor.Id}] Volume support detected via VCP 0x62"); + } + + // Check for Color Temperature support (VCP 0x14) + if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeSelectColorPreset)) + { + monitor.SupportsColorTemperature = true; + Logger.LogDebug($"[{monitor.Id}] Color temperature support detected via VCP 0x14"); + } + + Logger.LogInfo($"[{monitor.Id}] Capabilities updated: Contrast={monitor.SupportsContrast}, Volume={monitor.SupportsVolume}, ColorTemp={monitor.SupportsColorTemperature}"); + } + public void Dispose() { Dispose(true); diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpValueNames.cs b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpValueNames.cs index 772497177e..c3abdfbd31 100644 --- a/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpValueNames.cs +++ b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpValueNames.cs @@ -158,17 +158,34 @@ namespace PowerDisplay.Core.Utils /// /// VCP code (e.g., 0x14) /// Value to translate - /// Formatted string like "sRGB (0x01)" or "0x01" if unknown - public static string GetName(byte vcpCode, int value) + /// Name string like "sRGB" or null if unknown + public static string? GetName(byte vcpCode, int value) { if (ValueNames.TryGetValue(vcpCode, out var codeValues)) { if (codeValues.TryGetValue(value, out var name)) { - return $"{name} (0x{value:X2})"; + return name; } } + return null; + } + + /// + /// Get formatted display name for a VCP value (with hex value in parentheses) + /// + /// VCP code (e.g., 0x14) + /// Value to translate + /// Formatted string like "sRGB (0x01)" or "0x01" if unknown + public static string GetFormattedName(byte vcpCode, int value) + { + var name = GetName(vcpCode, value); + if (name != null) + { + return $"{name} (0x{value:X2})"; + } + return $"0x{value:X2}"; } diff --git a/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs b/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs index 1c943b4030..5ec803a6d0 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using ManagedCommon; using PowerDisplay.Core.Interfaces; using PowerDisplay.Core.Models; +using PowerDisplay.Core.Utils; using PowerDisplay.Helpers; using static PowerDisplay.Native.NativeConstants; using static PowerDisplay.Native.NativeDelegates; @@ -41,18 +42,11 @@ namespace PowerDisplay.Native.DDC public string Name => "DDC/CI Monitor Controller"; - public MonitorType SupportedType => MonitorType.External; - /// /// Check if the specified monitor can be controlled /// public async Task CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default) { - if (monitor.Type != MonitorType.External) - { - return false; - } - return await Task.Run( () => { @@ -193,8 +187,8 @@ namespace PowerDisplay.Native.DDC // 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})"); + var presetName = VcpValueNames.GetFormattedName(0x14, (int)current); + Logger.LogInfo($"[{monitor.Id}] Color temperature via 0x14: {presetName}"); return new BrightnessInfo((int)current, 0, (int)max); } @@ -236,10 +230,10 @@ namespace PowerDisplay.Native.DDC } // Set VCP 0x14 value - var presetName = VcpValueNames.GetName(0x14, colorTemperature); + var presetName = VcpValueNames.GetFormattedName(0x14, colorTemperature); if (DdcCiNative.TrySetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, (uint)colorTemperature)) { - Logger.LogInfo($"[{monitor.Id}] Set color temperature to 0x{colorTemperature:X2} ({presetName}) via 0x14"); + Logger.LogInfo($"[{monitor.Id}] Set color temperature to {presetName} via 0x14"); return MonitorOperationResult.Success(); } diff --git a/src/modules/powerdisplay/PowerDisplay/Native/DDC/MonitorDiscoveryHelper.cs b/src/modules/powerdisplay/PowerDisplay/Native/DDC/MonitorDiscoveryHelper.cs index 18ffd19ba8..c8d1ab21d7 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/DDC/MonitorDiscoveryHelper.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/DDC/MonitorDiscoveryHelper.cs @@ -173,7 +173,6 @@ namespace PowerDisplay.Native.DDC Id = monitorId, HardwareId = hardwareId, Name = name.Trim(), - Type = MonitorType.External, CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50, MinBrightness = 0, MaxBrightness = 100, diff --git a/src/modules/powerdisplay/PowerDisplay/Native/WMI/WmiController.cs b/src/modules/powerdisplay/PowerDisplay/Native/WMI/WmiController.cs index 7e2c08016d..101530551c 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/WMI/WmiController.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/WMI/WmiController.cs @@ -30,14 +30,12 @@ namespace PowerDisplay.Native.WMI public string Name => "WMI Monitor Controller (WmiLight)"; - public MonitorType SupportedType => MonitorType.Internal; - /// /// Check if the specified monitor can be controlled /// public async Task CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default) { - if (monitor.Type != MonitorType.Internal) + if (monitor.CommunicationMethod != "WMI") { return false; } @@ -223,7 +221,7 @@ namespace PowerDisplay.Native.WMI { Id = $"WMI_{instanceName}", Name = name, - Type = MonitorType.Internal, + CurrentBrightness = currentBrightness, MinBrightness = 0, MaxBrightness = 100, diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs index 726496ac7b..8e161a4b66 100644 --- a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs +++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs @@ -162,7 +162,7 @@ namespace PowerDisplay { if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null) { - _ = mainWindow.ViewModel.ReloadMonitorSettingsAsync(); + mainWindow.ViewModel.ApplySettingsFromUI(); } }); }); diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml.cs index fca9a576b7..1c96251fe3 100644 --- a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml.cs +++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml.cs @@ -130,9 +130,8 @@ namespace PowerDisplay { try { - // Perform monitor scanning and settings reload + // Perform monitor scanning (which internally calls ReloadMonitorSettingsAsync) await _viewModel.RefreshMonitorsAsync(); - await _viewModel.ReloadMonitorSettingsAsync(); // Adjust window size after data is loaded (must run on UI thread) DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent()); @@ -336,11 +335,9 @@ namespace PowerDisplay } } - private async void OnUIRefreshRequested(object? sender, EventArgs e) + private void OnUIRefreshRequested(object? sender, EventArgs e) { - await _viewModel.ReloadMonitorSettingsAsync(); - - // Adjust window size after settings are reloaded (no delay needed!) + // Adjust window size when UI configuration changes (feature visibility toggles) DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent()); } @@ -543,7 +540,7 @@ namespace PowerDisplay foreach (var monitor in monitors) { message += $"• {monitor.Name}\n"; - message += $" Type: {monitor.Type}\n"; + message += $" Communication: {monitor.CommunicationMethod}\n"; message += $" Brightness: {monitor.CurrentBrightness}%\n\n"; } diff --git a/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs index 5a744bee8c..f4ab3ba5b2 100644 --- a/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs +++ b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs @@ -16,7 +16,6 @@ namespace PowerDisplay.Serialization /// JSON source generation context for AOT compatibility. /// Eliminates reflection-based JSON serialization. /// - [JsonSerializable(typeof(PowerDisplayMonitorsIPCResponse))] [JsonSerializable(typeof(MonitorInfoData))] [JsonSerializable(typeof(IPCMessageAction))] [JsonSerializable(typeof(MonitorStateFile))] diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs index 89e940f7e7..1270ffc97a 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs @@ -23,6 +23,7 @@ using PowerDisplay.Core.Interfaces; using PowerDisplay.Core.Models; using PowerDisplay.Helpers; using PowerDisplay.Serialization; +using PowerToys.Interop; using Monitor = PowerDisplay.Core.Models.Monitor; namespace PowerDisplay.ViewModels; @@ -38,7 +39,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable private readonly CancellationTokenSource _cancellationTokenSource; private readonly ISettingsUtils _settingsUtils; private readonly MonitorStateManager _stateManager; - private FileSystemWatcher? _settingsWatcher; private ObservableCollection _monitors; private string _statusText; @@ -69,9 +69,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable // Subscribe to events _monitorManager.MonitorsChanged += OnMonitorsChanged; - // Setup settings file monitoring - SetupSettingsFileWatcher(); - // Start initial discovery _ = InitializeAsync(); } @@ -221,14 +218,28 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable { Monitors.Clear(); + // Load settings to check for hidden monitors + var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName); + var hiddenMonitorIds = new HashSet( + settings.Properties.Monitors + .Where(m => m.IsHidden) + .Select(m => m.HardwareId)); + var colorTempTasks = new List(); foreach (var monitor in monitors) { + // Skip monitors that are marked as hidden in settings + if (hiddenMonitorIds.Contains(monitor.HardwareId)) + { + Logger.LogInfo($"[UpdateMonitorList] Skipping hidden monitor: {monitor.Name} (HardwareId: {monitor.HardwareId})"); + continue; + } + var vm = new MonitorViewModel(monitor, _monitorManager, this); Monitors.Add(vm); // Asynchronously initialize color temperature for DDC/CI monitors - if (monitor.SupportsColorTemperature && monitor.Type == MonitorType.External) + if (monitor.SupportsColorTemperature && monitor.CommunicationMethod == "DDC/CI") { var task = InitializeColorTemperatureSafeAsync(monitor.Id, vm); colorTempTasks.Add(task); @@ -264,11 +275,25 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable { _dispatcherQueue.TryEnqueue(() => { + // Load settings to check for hidden monitors + var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName); + var hiddenMonitorIds = new HashSet( + settings.Properties.Monitors + .Where(m => m.IsHidden) + .Select(m => m.HardwareId)); + // Handle monitors being added or removed if (e.AddedMonitors.Count > 0) { foreach (var monitor in e.AddedMonitors) { + // Skip monitors that are marked as hidden + if (hiddenMonitorIds.Contains(monitor.HardwareId)) + { + Logger.LogInfo($"[OnMonitorsChanged] Skipping hidden monitor (added): {monitor.Name} (HardwareId: {monitor.HardwareId})"); + continue; + } + var existingVm = GetMonitorViewModel(monitor.Id); if (existingVm == null) { @@ -311,110 +336,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable return null; } - /// - /// Setup settings file watcher - /// - private void SetupSettingsFileWatcher() - { - try - { - var settingsPath = _settingsUtils.GetSettingsFilePath("PowerDisplay"); - var directory = Path.GetDirectoryName(settingsPath); - var fileName = Path.GetFileName(settingsPath); - - if (!string.IsNullOrEmpty(directory)) - { - // Ensure directory exists - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - _settingsWatcher = new FileSystemWatcher(directory, fileName) - { - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, - EnableRaisingEvents = true, - }; - - _settingsWatcher.Changed += OnSettingsFileChanged; - _settingsWatcher.Created += OnSettingsFileChanged; - - Logger.LogInfo($"Settings file watcher setup for: {settingsPath}"); - } - } - catch (Exception ex) - { - Logger.LogError($"Failed to setup settings file watcher: {ex.Message}"); - } - } - - /// - /// Handle settings file changes - only monitors UI configuration changes from Settings UI - /// (monitor_state.json is managed separately and doesn't trigger this) - /// - private void OnSettingsFileChanged(object sender, FileSystemEventArgs e) - { - try - { - Logger.LogInfo($"Settings file changed by Settings UI: {e.FullPath}"); - - // Add small delay to ensure file write completion - Task.Delay(200).ContinueWith(_ => - { - try - { - // Read updated settings - var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay"); - - _dispatcherQueue.TryEnqueue(() => - { - // Update feature visibility for each monitor (UI configuration only) - foreach (var monitorVm in Monitors) - { - // Use HardwareId for lookup (unified identification) - Logger.LogInfo($"[Settings Update] Looking for monitor settings with Hardware ID: '{monitorVm.HardwareId}'"); - - var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m => - m.HardwareId == monitorVm.HardwareId); - - if (monitorSettings != null) - { - Logger.LogInfo($"[Settings Update] Found monitor settings for Hardware ID '{monitorVm.HardwareId}': ColorTemp={monitorSettings.EnableColorTemperature}, Contrast={monitorSettings.EnableContrast}, Volume={monitorSettings.EnableVolume}"); - - // Update visibility flags based on Settings UI toggles - monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature; - monitorVm.ShowContrast = monitorSettings.EnableContrast; - monitorVm.ShowVolume = monitorSettings.EnableVolume; - } - else - { - Logger.LogWarning($"[Settings Update] No monitor settings found for Hardware ID '{monitorVm.HardwareId}'"); - Logger.LogInfo($"[Settings Update] Available monitors in settings:"); - foreach (var availableMonitor in settings.Properties.Monitors) - { - Logger.LogInfo($" - Hardware: '{availableMonitor.HardwareId}', Name: '{availableMonitor.Name}'"); - } - } - } - - // Trigger UI refresh for configuration changes - UIRefreshRequested?.Invoke(this, EventArgs.Empty); - }); - - Logger.LogInfo($"Settings UI configuration reloaded, monitor count: {settings.Properties.Monitors.Count}"); - } - catch (Exception ex) - { - Logger.LogError($"Failed to reload settings: {ex.Message}"); - } - }); - } - catch (Exception ex) - { - Logger.LogError($"Error handling settings file change: {ex.Message}"); - } - } - /// /// Safe wrapper for initializing color temperature asynchronously /// @@ -450,7 +371,173 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable } /// - /// Reload monitor settings from configuration + /// Apply all settings changes from Settings UI (IPC event handler entry point) + /// Coordinates both UI configuration and hardware parameter updates + /// + public async void ApplySettingsFromUI() + { + try + { + Logger.LogInfo("[Settings] Processing settings update from Settings UI"); + + var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay"); + + // 1. Apply UI configuration changes (synchronous, lightweight) + 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) + { + Logger.LogError($"[Settings] Failed to apply settings from UI: {ex.Message}"); + } + } + + /// + /// Apply UI-only configuration changes (feature visibility toggles) + /// Synchronous, lightweight operation + /// + private void ApplyUIConfiguration(PowerDisplaySettings settings) + { + try + { + Logger.LogInfo("[Settings] Applying UI configuration changes (feature visibility)"); + + foreach (var monitorVm in Monitors) + { + ApplyFeatureVisibility(monitorVm, settings); + } + + // Trigger UI refresh + UIRefreshRequested?.Invoke(this, EventArgs.Empty); + + Logger.LogInfo("[Settings] UI configuration applied"); + } + catch (Exception ex) + { + Logger.LogError($"[Settings] Failed to apply UI configuration: {ex.Message}"); + } + } + + /// + /// Apply hardware parameter changes (brightness, color temperature) + /// Asynchronous operation that communicates with monitor hardware via DDC/CI + /// Note: Contrast and volume are not currently adjustable from Settings UI + /// + private async Task ApplyHardwareParametersAsync(PowerDisplaySettings settings) + { + try + { + Logger.LogInfo("[Settings] Applying hardware parameter changes"); + + 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 brightness if changed + if (monitorSettings.CurrentBrightness >= 0 && + monitorSettings.CurrentBrightness != monitorVm.Brightness) + { + Logger.LogInfo($"[Settings] Scheduling brightness update for {hardwareId}: {monitorSettings.CurrentBrightness}%"); + + var task = ApplyBrightnessAsync(monitorVm, monitorSettings.CurrentBrightness); + updateTasks.Add(task); + } + + // Apply color temperature if changed and feature is enabled + if (monitorVm.ShowColorTemperature && + monitorSettings.ColorTemperature > 0 && + monitorSettings.ColorTemperature != monitorVm.ColorTemperature) + { + Logger.LogInfo($"[Settings] Scheduling color temperature update for {hardwareId}: 0x{monitorSettings.ColorTemperature:X2}"); + + var task = ApplyColorTemperatureAsync(monitorVm, monitorSettings.ColorTemperature); + updateTasks.Add(task); + } + + // Note: Contrast and volume are adjusted in real-time via flyout UI, + // not from Settings UI, so they don't need IPC handling here + } + + // Wait for all hardware updates to complete + if (updateTasks.Count > 0) + { + await Task.WhenAll(updateTasks); + Logger.LogInfo($"[Settings] Completed {updateTasks.Count} hardware parameter updates"); + } + else + { + Logger.LogInfo("[Settings] No hardware parameter changes detected"); + } + } + catch (Exception ex) + { + Logger.LogError($"[Settings] Failed to apply hardware parameters: {ex.Message}"); + } + } + + /// + /// Apply brightness to a specific monitor + /// + private async Task ApplyBrightnessAsync(MonitorViewModel monitorVm, int brightness) + { + // Use MonitorViewModel's unified method with immediate application (no debounce for IPC) + await monitorVm.SetBrightnessAsync(brightness, immediate: true); + } + + /// + /// Apply color temperature to a specific monitor + /// + private async Task ApplyColorTemperatureAsync(MonitorViewModel monitorVm, int colorTemperature) + { + // Use MonitorViewModel's unified method + await monitorVm.SetColorTemperatureAsync(colorTemperature); + } + + /// + /// Apply Settings UI configuration changes (feature visibility toggles only) + /// OBSOLETE: Use ApplySettingsFromUI() instead + /// + [Obsolete("Use ApplySettingsFromUI() instead - this method only handles UI config, not hardware parameters")] + public void ApplySettingsUIConfiguration() + { + try + { + Logger.LogInfo("[Settings] Applying Settings UI configuration changes (feature visibility only)"); + + // Read current settings + var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay"); + + // Update feature visibility for each monitor (UI configuration only) + foreach (var monitorVm in Monitors) + { + ApplyFeatureVisibility(monitorVm, settings); + } + + // Trigger UI refresh for configuration changes + UIRefreshRequested?.Invoke(this, EventArgs.Empty); + + Logger.LogInfo($"[Settings] Settings UI configuration applied, monitor count: {settings.Properties.Monitors.Count}"); + } + catch (Exception ex) + { + Logger.LogError($"[Settings] Failed to apply Settings UI configuration: {ex.Message}"); + } + } + + /// + /// Reload monitor settings from configuration - ONLY called at startup /// /// Optional tasks for color temperature initialization to wait for public async Task ReloadMonitorSettingsAsync(List? colorTempInitTasks = null) @@ -666,11 +753,12 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable { try { - if (Monitors.Count == 0) - { - Logger.LogInfo("No monitors to save to settings.json"); - return; - } + // Load current settings to preserve user preferences (including IsHidden) + var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName); + + // Create lookup of existing monitors by HardwareId to preserve settings + var existingMonitorSettings = settings.Properties.Monitors + .ToDictionary(m => m.HardwareId, m => m); // Build monitor list using Settings UI's MonitorInfo model var monitors = new List(); @@ -681,8 +769,8 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable name: vm.Name, internalName: vm.Id, hardwareId: vm.HardwareId, - communicationMethod: GetCommunicationMethodString(vm.Type), - monitorType: vm.Type.ToString(), + communicationMethod: vm.CommunicationMethod, + monitorType: vm.IsInternal ? "Internal" : "External", currentBrightness: vm.Brightness, colorTemperature: vm.ColorTemperature) { @@ -697,11 +785,28 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable .ToList() ?? new List(), }; + // Preserve user settings from existing monitor if available + if (existingMonitorSettings.TryGetValue(vm.HardwareId, out var existingMonitor)) + { + monitorInfo.IsHidden = existingMonitor.IsHidden; + monitorInfo.EnableColorTemperature = existingMonitor.EnableColorTemperature; + monitorInfo.EnableContrast = existingMonitor.EnableContrast; + monitorInfo.EnableVolume = existingMonitor.EnableVolume; + } + monitors.Add(monitorInfo); } - // Load current settings - var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName); + // Also add hidden monitors from existing settings (monitors that are hidden but still connected) + foreach (var existingMonitor in settings.Properties.Monitors.Where(m => m.IsHidden)) + { + // Only add if not already in the list (to avoid duplicates) + if (!monitors.Any(m => m.HardwareId == existingMonitor.HardwareId)) + { + monitors.Add(existingMonitor); + Logger.LogInfo($"[SaveMonitorsToSettings] Preserving hidden monitor in settings: {existingMonitor.Name} (HardwareId: {existingMonitor.HardwareId})"); + } + } // Update monitors list settings.Properties.Monitors = monitors; @@ -711,7 +816,10 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable System.Text.Json.JsonSerializer.Serialize(settings, AppJsonContext.Default.PowerDisplaySettings), PowerDisplaySettings.ModuleName); - Logger.LogInfo($"Saved {Monitors.Count} monitors to settings.json"); + Logger.LogInfo($"Saved {monitors.Count} monitors to settings.json ({Monitors.Count} visible, {monitors.Count - Monitors.Count} hidden)"); + + // Signal Settings UI that monitor list has been updated + SignalMonitorsRefreshEvent(); } catch (Exception ex) { @@ -719,6 +827,31 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable } } + /// + /// Signal Settings UI that the monitor list has been refreshed + /// + private void SignalMonitorsRefreshEvent() + { + // TODO: Re-enable when Constants class is properly defined + /* + try + { + using (var eventHandle = new System.Threading.EventWaitHandle( + false, + System.Threading.EventResetMode.AutoReset, + Constants.RefreshPowerDisplayMonitorsEvent())) + { + eventHandle.Set(); + Logger.LogInfo("Signaled refresh monitors event to Settings UI"); + } + } + catch (Exception ex) + { + Logger.LogError($"Failed to signal refresh monitors event: {ex.Message}"); + } + */ + } + /// /// Format VCP code information for display in Settings UI /// @@ -738,12 +871,13 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable else if (info.HasDiscreteValues) { var formattedValues = info.SupportedValues - .Select(v => Core.Utils.VcpValueNames.GetName(code, v)) + .Select(v => Core.Utils.VcpValueNames.GetFormattedName(code, v)) .ToList(); result.Values = $"Values: {string.Join(", ", formattedValues)}"; result.HasValues = true; // Populate value list for Settings UI ComboBox + // Store raw name (without formatting) so Settings UI can format it consistently result.ValueList = info.SupportedValues .Select(v => new Microsoft.PowerToys.Settings.UI.Library.VcpValueInfo { @@ -760,16 +894,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable return result; } - private string GetCommunicationMethodString(MonitorType type) - { - return type switch - { - MonitorType.External => "DDC/CI", - MonitorType.Internal => "WMI", - _ => "Unknown", - }; - } - // IDisposable public void Dispose() { @@ -778,10 +902,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable // Cancel all async operations first _cancellationTokenSource?.Cancel(); - // Stop file monitoring immediately - _settingsWatcher?.Dispose(); - _settingsWatcher = null; - // No need to flush state - MonitorStateManager now saves directly on each update! // State is already persisted, no pending changes to wait for. diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs index 94162214dc..70e2a1f53d 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs @@ -49,7 +49,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable { switch (propertyName) { - // ColorTemperature removed - now controlled via Settings UI case nameof(Brightness): _brightness = value; OnPropertyChanged(nameof(Brightness)); @@ -63,6 +62,205 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable _volume = value; OnPropertyChanged(nameof(Volume)); break; + case nameof(ColorTemperature): + // Update underlying monitor model + _monitor.CurrentColorTemperature = value; + OnPropertyChanged(nameof(ColorTemperature)); + OnPropertyChanged(nameof(ColorTemperaturePresetName)); + break; + } + } + + /// + /// Unified method to apply brightness with hardware update and state persistence. + /// Can be called from Flyout UI (with debounce) or Settings UI/IPC (immediate). + /// + /// Brightness value (0-100) + /// If true, applies immediately; if false, debounces for smooth slider + public async Task SetBrightnessAsync(int brightness, bool immediate = false) + { + brightness = Math.Clamp(brightness, MinBrightness, MaxBrightness); + + // Update UI state immediately for smooth response + if (_brightness != brightness) + { + _brightness = brightness; + OnPropertyChanged(nameof(Brightness)); + } + + // Apply to hardware (with or without debounce) + if (immediate) + { + await ApplyBrightnessToHardwareAsync(brightness); + } + else + { + // Debounce for slider smoothness + var capturedValue = brightness; + _brightnessDebouncer.Debounce(async () => await ApplyBrightnessToHardwareAsync(capturedValue)); + } + } + + /// + /// Unified method to apply contrast with hardware update and state persistence. + /// + public async Task SetContrastAsync(int contrast, bool immediate = false) + { + contrast = Math.Clamp(contrast, MinContrast, MaxContrast); + + if (_contrast != contrast) + { + _contrast = contrast; + OnPropertyChanged(nameof(Contrast)); + OnPropertyChanged(nameof(ContrastPercent)); + } + + if (immediate) + { + await ApplyContrastToHardwareAsync(contrast); + } + else + { + var capturedValue = contrast; + _contrastDebouncer.Debounce(async () => await ApplyContrastToHardwareAsync(capturedValue)); + } + } + + /// + /// Unified method to apply volume with hardware update and state persistence. + /// + public async Task SetVolumeAsync(int volume, bool immediate = false) + { + volume = Math.Clamp(volume, MinVolume, MaxVolume); + + if (_volume != volume) + { + _volume = volume; + OnPropertyChanged(nameof(Volume)); + } + + if (immediate) + { + await ApplyVolumeToHardwareAsync(volume); + } + else + { + var capturedValue = volume; + _volumeDebouncer.Debounce(async () => await ApplyVolumeToHardwareAsync(capturedValue)); + } + } + + /// + /// Unified method to apply color temperature with hardware update and state persistence. + /// Always immediate (no debouncing for discrete preset values). + /// + public async Task SetColorTemperatureAsync(int colorTemperature) + { + try + { + Logger.LogInfo($"[{HardwareId}] Setting color temperature to 0x{colorTemperature:X2}"); + + var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature); + + if (result.IsSuccess) + { + _monitor.CurrentColorTemperature = colorTemperature; + OnPropertyChanged(nameof(ColorTemperature)); + OnPropertyChanged(nameof(ColorTemperaturePresetName)); + + _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "ColorTemperature", colorTemperature); + Logger.LogInfo($"[{HardwareId}] Color temperature applied successfully"); + } + else + { + Logger.LogWarning($"[{HardwareId}] Failed to set color temperature: {result.ErrorMessage}"); + } + } + catch (Exception ex) + { + Logger.LogError($"[{HardwareId}] Exception setting color temperature: {ex.Message}"); + } + } + + /// + /// Internal method - applies brightness to hardware and persists state. + /// Unified logic for all sources (Flyout, Settings, etc.). + /// + private async Task ApplyBrightnessToHardwareAsync(int brightness) + { + try + { + Logger.LogDebug($"[{HardwareId}] Applying brightness: {brightness}%"); + + var result = await _monitorManager.SetBrightnessAsync(Id, brightness); + + if (result.IsSuccess) + { + _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Brightness", brightness); + Logger.LogTrace($"[{HardwareId}] Brightness applied and saved"); + } + else + { + Logger.LogWarning($"[{HardwareId}] Failed to set brightness: {result.ErrorMessage}"); + } + } + catch (Exception ex) + { + Logger.LogError($"[{HardwareId}] Exception setting brightness: {ex.Message}"); + } + } + + /// + /// Internal method - applies contrast to hardware and persists state. + /// + private async Task ApplyContrastToHardwareAsync(int contrast) + { + try + { + Logger.LogDebug($"[{HardwareId}] Applying contrast: {contrast}%"); + + var result = await _monitorManager.SetContrastAsync(Id, contrast); + + if (result.IsSuccess) + { + _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Contrast", contrast); + Logger.LogTrace($"[{HardwareId}] Contrast applied and saved"); + } + else + { + Logger.LogWarning($"[{HardwareId}] Failed to set contrast: {result.ErrorMessage}"); + } + } + catch (Exception ex) + { + Logger.LogError($"[{HardwareId}] Exception setting contrast: {ex.Message}"); + } + } + + /// + /// Internal method - applies volume to hardware and persists state. + /// + private async Task ApplyVolumeToHardwareAsync(int volume) + { + try + { + Logger.LogDebug($"[{HardwareId}] Applying volume: {volume}%"); + + var result = await _monitorManager.SetVolumeAsync(Id, volume); + + if (result.IsSuccess) + { + _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Volume", volume); + Logger.LogTrace($"[{HardwareId}] Volume applied and saved"); + } + else + { + Logger.LogWarning($"[{HardwareId}] Failed to set volume: {result.ErrorMessage}"); + } + } + catch (Exception ex) + { + Logger.LogError($"[{HardwareId}] Exception setting volume: {ex.Message}"); } } @@ -108,18 +306,21 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable public string Manufacturer => _monitor.Manufacturer; - public MonitorType Type => _monitor.Type; + public string CommunicationMethod => _monitor.CommunicationMethod; - public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External"; + public bool IsInternal => _monitor.CommunicationMethod == "WMI"; public string? CapabilitiesRaw => _monitor.CapabilitiesRaw; public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo; /// - /// Gets the icon glyph based on monitor type + /// Gets the icon glyph based on communication method + /// WMI monitors (laptop internal displays) use laptop icon, others use external monitor icon /// - public string MonitorIconGlyph => Type == MonitorType.Internal ? "\uEA37" : "\uE7F4"; + public string MonitorIconGlyph => _monitor.CommunicationMethod?.Contains("WMI", StringComparison.OrdinalIgnoreCase) == true + ? "\uEA37" // Laptop icon for WMI + : "\uE7F4"; // External monitor icon for DDC/CI and others // Monitor property ranges public int MinBrightness => _monitor.MinBrightness; @@ -186,24 +387,8 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable { if (_brightness != value) { - // Update UI state immediately - keep slider smooth - _brightness = value; - OnPropertyChanged(); // UI responds immediately - - // Debounce hardware update - much simpler than complex queue! - var capturedValue = value; // Capture value for async closure - _brightnessDebouncer.Debounce(async () => - { - try - { - await _monitorManager.SetBrightnessAsync(Id, capturedValue); - _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Brightness", capturedValue); - } - catch (Exception ex) - { - Logger.LogError($"Failed to set brightness for {Id}: {ex.Message}"); - } - }); + // Use unified method with debouncing for smooth slider + _ = SetBrightnessAsync(value, immediate: false); } } } @@ -215,6 +400,11 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable /// public int ColorTemperature => _monitor.CurrentColorTemperature; + /// + /// Human-readable color temperature preset name (e.g., "6500K", "sRGB") + /// + public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName; + public int Contrast { get => _contrast; @@ -222,23 +412,8 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable { if (_contrast != value) { - _contrast = value; - OnPropertyChanged(); - - // Debounce hardware update - var capturedValue = value; - _contrastDebouncer.Debounce(async () => - { - try - { - await _monitorManager.SetContrastAsync(Id, capturedValue); - _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Contrast", capturedValue); - } - catch (Exception ex) - { - Logger.LogError($"Failed to set contrast for {Id}: {ex.Message}"); - } - }); + // Use unified method with debouncing + _ = SetContrastAsync(value, immediate: false); } } } @@ -250,23 +425,8 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable { if (_volume != value) { - _volume = value; - OnPropertyChanged(); - - // Debounce hardware update - var capturedValue = value; - _volumeDebouncer.Debounce(async () => - { - try - { - await _monitorManager.SetVolumeAsync(Id, capturedValue); - _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Volume", capturedValue); - } - catch (Exception ex) - { - Logger.LogError($"Failed to set volume for {Id}: {ex.Message}"); - } - }); + // Use unified method with debouncing + _ = SetVolumeAsync(value, immediate: false); } } } diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp index a32648b289..f17697f1e3 100644 --- a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp +++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp @@ -323,7 +323,6 @@ public: parse_hotkey_settings(values); parse_activation_hotkey(values); - values.save_to_settings_file(); // Signal settings updated event if (m_hSettingsUpdatedEvent) diff --git a/src/settings-ui/Settings.UI.Library/MonitorInfo.cs b/src/settings-ui/Settings.UI.Library/MonitorInfo.cs index 38274ae1a3..326e43cf4d 100644 --- a/src/settings-ui/Settings.UI.Library/MonitorInfo.cs +++ b/src/settings-ui/Settings.UI.Library/MonitorInfo.cs @@ -387,6 +387,41 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonIgnore] public string VolumeTooltip => _supportsVolume ? string.Empty : "Volume control not supported by this monitor"; + /// + /// Generate formatted text of all VCP codes for clipboard + /// + public string GetVcpCodesAsText() + { + if (_vcpCodesFormatted == null || _vcpCodesFormatted.Count == 0) + { + return "No VCP codes detected"; + } + + var lines = new List(); + lines.Add($"VCP Capabilities for {_name}"); + lines.Add($"Monitor: {_name}"); + lines.Add($"Hardware ID: {_hardwareId}"); + lines.Add(string.Empty); + lines.Add("Detected VCP Codes:"); + lines.Add(new string('-', 50)); + + foreach (var vcp in _vcpCodesFormatted) + { + lines.Add(string.Empty); + lines.Add(vcp.Title); + if (vcp.HasValues) + { + lines.Add($" {vcp.Values}"); + } + } + + lines.Add(string.Empty); + lines.Add(new string('-', 50)); + lines.Add($"Total: {_vcpCodesFormatted.Count} VCP codes"); + + return string.Join(System.Environment.NewLine, lines); + } + /// /// Represents a color temperature preset item for VCP code 0x14 /// diff --git a/src/settings-ui/Settings.UI.Library/PowerDisplayMonitorsIPCResponse.cs b/src/settings-ui/Settings.UI.Library/PowerDisplayMonitorsIPCResponse.cs deleted file mode 100644 index a06cb056e2..0000000000 --- a/src/settings-ui/Settings.UI.Library/PowerDisplayMonitorsIPCResponse.cs +++ /dev/null @@ -1,27 +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.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.PowerToys.Settings.UI.Library -{ - /// - /// IPC Response message for PowerDisplay monitors information - /// - public class PowerDisplayMonitorsIPCResponse - { - [JsonPropertyName("response_type")] - public string ResponseType { get; set; } = "powerdisplay_monitors"; - - [JsonPropertyName("monitors")] - public List Monitors { get; set; } = new List(); - - public PowerDisplayMonitorsIPCResponse(List monitors) - { - Monitors = monitors; - } - } -} diff --git a/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs index 2636643f66..612b48ef8a 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs @@ -133,7 +133,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonSerializable(typeof(KeyboardKeysProperty))] [JsonSerializable(typeof(MonitorInfo))] [JsonSerializable(typeof(MonitorInfoData))] - [JsonSerializable(typeof(PowerDisplayMonitorsIPCResponse))] [JsonSerializable(typeof(PowerDisplayActionMessage))] [JsonSerializable(typeof(SettingsUILibraryHelpers.SearchLocation))] [JsonSerializable(typeof(SndLightSwitchSettings))] diff --git a/src/settings-ui/Settings.UI/Services/IPCResponseService.cs b/src/settings-ui/Settings.UI/Services/IPCResponseService.cs index 5a7bbfa2ab..8583f9dde2 100644 --- a/src/settings-ui/Settings.UI/Services/IPCResponseService.cs +++ b/src/settings-ui/Settings.UI/Services/IPCResponseService.cs @@ -23,8 +23,6 @@ namespace Microsoft.PowerToys.Settings.UI.Services public static event EventHandler AllHotkeyConflictsReceived; - public static event EventHandler PowerDisplayMonitorsReceived; - public void RegisterForIPC() { ShellPage.ShellHandler?.IPCResponseHandleList.Add(ProcessIPCMessage); @@ -52,10 +50,6 @@ namespace Microsoft.PowerToys.Settings.UI.Services { ProcessAllHotkeyConflicts(json); } - else if (responseType.Equals("powerdisplay_monitors", StringComparison.Ordinal)) - { - ProcessPowerDisplayMonitors(json); - } } } catch (Exception ex) @@ -205,36 +199,5 @@ namespace Microsoft.PowerToys.Settings.UI.Services return conflictGroup; } - - private void ProcessPowerDisplayMonitors(JsonObject json) - { - try - { - var jsonString = json.Stringify(); - var response = System.Text.Json.JsonSerializer.Deserialize(jsonString); - - if (response?.Monitors == null) - { - PowerDisplayMonitorsReceived?.Invoke(this, Array.Empty()); - return; - } - - var monitors = response.Monitors.Select(m => - new MonitorInfo( - m.Name, - m.InternalName, - m.HardwareId, - m.CommunicationMethod, - m.MonitorType, - m.CurrentBrightness, - m.ColorTemperature)).ToArray(); - - PowerDisplayMonitorsReceived?.Invoke(this, monitors); - } - catch (Exception ex) - { - Debug.WriteLine($"[IPCResponseService] Failed to parse PowerDisplay monitors response: {ex.Message}"); - } - } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml index 40954ce259..97f74a8f3e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml @@ -123,21 +123,36 @@ x:Uid="PowerDisplay_Monitor_ColorTemperature" IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}"> - + + + + + + + + + + + IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}" + SelectionChanged="ColorTemperatureComboBox_SelectionChanged" + Tag="{x:Bind}"> - + @@ -201,9 +216,8 @@ + + + + + - + + FontWeight="SemiBold" + FontSize="11" + TextWrapping="Wrap" /> + FontSize="10" + Foreground="{ThemeResource TextFillColorSecondaryBrush}" + TextWrapping="Wrap" + Margin="12,2,0,0" + Visibility="{x:Bind HasValues}" /> - - + + 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 7259d394a5..10d506e36f 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs @@ -2,11 +2,16 @@ // 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 System.Threading.Tasks; using CommunityToolkit.WinUI.Controls; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel.DataTransfer; namespace Microsoft.PowerToys.Settings.UI.Views { @@ -14,6 +19,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views { private PowerDisplayViewModel ViewModel { get; set; } + // Track previous color temperature values to restore on cancel + private Dictionary _previousColorTemperatureValues = new Dictionary(); + + // Flag to prevent recursive SelectionChanged events + private bool _isUpdatingColorTemperature; + public PowerDisplayPage() { var settingsUtils = new SettingsUtils(); @@ -30,5 +41,112 @@ namespace Microsoft.PowerToys.Settings.UI.Views { ViewModel.RefreshEnabledState(); } + + private void CopyVcpCodes_Click(object sender, RoutedEventArgs e) + { + if (sender is Button button && button.Tag is MonitorInfo monitor) + { + var vcpText = monitor.GetVcpCodesAsText(); + var dataPackage = new DataPackage(); + dataPackage.SetText(vcpText); + Clipboard.SetContent(dataPackage); + } + } + + private async void ColorTemperatureComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is ComboBox comboBox && comboBox.Tag is MonitorInfo monitor) + { + // Skip if we are programmatically updating the value + if (_isUpdatingColorTemperature) + { + return; + } + + // Skip if this is the initial load (no removed items means programmatic selection) + if (e.RemovedItems.Count == 0) + { + // Store the initial value + if (!_previousColorTemperatureValues.ContainsKey(monitor.HardwareId)) + { + _previousColorTemperatureValues[monitor.HardwareId] = monitor.ColorTemperature; + } + + return; + } + + // Get the new selected value + var newValue = comboBox.SelectedValue as int?; + if (!newValue.HasValue) + { + return; + } + + // Get the previous value + int previousValue; + if (!_previousColorTemperatureValues.TryGetValue(monitor.HardwareId, out previousValue)) + { + previousValue = monitor.ColorTemperature; + } + + // Show confirmation dialog + var dialog = new ContentDialog + { + XamlRoot = this.XamlRoot, + Title = "Confirm Color Temperature Change", + Content = new StackPanel + { + Spacing = 12, + Children = + { + new TextBlock + { + Text = "⚠️ Warning: This is a potentially dangerous operation!", + FontWeight = Microsoft.UI.Text.FontWeights.Bold, + Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["SystemFillColorCriticalBrush"], + TextWrapping = TextWrapping.Wrap, + }, + new TextBlock + { + Text = "Changing the color temperature setting may cause unpredictable results including:", + TextWrapping = TextWrapping.Wrap, + }, + new TextBlock + { + Text = "• Incorrect display colors\n• Display malfunction\n• Settings that cannot be reverted", + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(20, 0, 0, 0), + }, + new TextBlock + { + Text = "Are you sure you want to proceed with this change?", + FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, + TextWrapping = TextWrapping.Wrap, + }, + }, + }, + PrimaryButtonText = "Yes, Change Setting", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Close, + }; + + var result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) + { + // User confirmed, apply the change + monitor.ColorTemperature = newValue.Value; + _previousColorTemperatureValues[monitor.HardwareId] = newValue.Value; + } + else + { + // User cancelled, revert to previous value + // Set flag to prevent recursive event + _isUpdatingColorTemperature = true; + comboBox.SelectedValue = previousValue; + _isUpdatingColorTemperature = false; + } + } + } } } diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs index 5fdfe8eda4..1966d60ea9 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs @@ -14,12 +14,14 @@ using System.Threading.Tasks; using System.Windows; using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; using Microsoft.PowerToys.Settings.UI.SerializationContext; using Microsoft.PowerToys.Settings.UI.Services; +using PowerToys.Interop; namespace Microsoft.PowerToys.Settings.UI.ViewModels { @@ -44,13 +46,28 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels InitializeEnabledValue(); // Initialize monitors collection using property setter for proper subscription setup - Monitors = new ObservableCollection(_settings.Properties.Monitors); + // Parse capabilities for each loaded monitor to ensure UI displays correctly + var loadedMonitors = _settings.Properties.Monitors; + foreach (var monitor in loadedMonitors) + { + ParseFeatureSupportFromCapabilities(monitor); + PopulateColorPresetsForMonitor(monitor); + } + + Monitors = new ObservableCollection(loadedMonitors); // set the callback functions value to handle outgoing IPC message. SendConfigMSG = ipcMSGCallBackFunc; - // Subscribe to monitor information updates - IPCResponseService.PowerDisplayMonitorsReceived += OnMonitorsReceived; + // TODO: Re-enable monitor refresh events when Logger and Constants are properly defined + // Listen for monitor refresh events from PowerDisplay.exe + // NativeEventWaiter.WaitForEventLoop( + // Constants.RefreshPowerDisplayMonitorsEvent(), + // () => + // { + // Logger.LogInfo("Received refresh monitors event from PowerDisplay.exe"); + // ReloadMonitorsFromSettings(); + // }); } private void InitializeEnabledValue() @@ -147,11 +164,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } - private void OnMonitorsReceived(object sender, MonitorInfo[] monitors) - { - UpdateMonitors(monitors); - } - private void Monitors_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { SubscribeToItemPropertyChanged(e.NewItems?.Cast()); @@ -307,7 +319,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (int.TryParse(valueInfo.Value?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int vcpValue)) { - var displayName = valueInfo.Name ?? $"0x{vcpValue:X2}"; + // Format display name for Settings UI + var displayName = FormatColorTemperatureDisplayName(valueInfo.Name, vcpValue); presetList.Add(new MonitorInfo.ColorPresetItem(vcpValue, displayName)); } } @@ -320,6 +333,28 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels monitor.AvailableColorPresets = new ObservableCollection(presetList); } + /// + /// 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() { // Unsubscribe from monitor property changes @@ -330,9 +365,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { _monitors.CollectionChanged -= Monitors_CollectionChanged; } - - // Unsubscribe from events - IPCResponseService.PowerDisplayMonitorsReceived -= OnMonitorsReceived; } /// @@ -397,6 +429,42 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels SendConfigMSG(JsonSerializer.Serialize(actionMessage)); } + /// + /// Reload monitor list from settings file (called when PowerDisplay.exe signals monitor changes) + /// + private void ReloadMonitorsFromSettings() + { + try + { + // TODO: Re-enable logging when Logger is properly defined + // Logger.LogInfo("Reloading monitors from settings file"); + + // Read fresh settings from file + var updatedSettings = SettingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName); + var updatedMonitors = updatedSettings.Properties.Monitors; + + // 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 internal settings reference + _settings.Properties.Monitors = updatedMonitors; + + // Logger.LogInfo($"Successfully reloaded {updatedMonitors.Count} monitors"); + } + catch (Exception) + { + // Logger.LogError($"Failed to reload monitors from settings: {ex.Message}"); + } + } + private Func SendConfigMSG { get; } private bool _isPowerDisplayEnabled; @@ -425,16 +493,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private void NotifySettingsChanged() { + // Persist locally first so settings survive even if the module DLL isn't loaded yet. + SettingsUtils.SaveSettings(_settings.ToJsonString(), PowerDisplaySettings.ModuleName); + // Using InvariantCulture as this is an IPC message + // This message will be intercepted by the runner, which passes the serialized JSON to + // PowerDisplay Module Interface's set_config() method, which then applies it in-process. SendConfigMSG( string.Format( CultureInfo.InvariantCulture, "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", PowerDisplaySettings.ModuleName, JsonSerializer.Serialize(_settings, SourceGenerationContextContext.Default.PowerDisplaySettings))); - - // Save settings using the standard settings utility - SettingsUtils.SaveSettings(_settings.ToJsonString(), PowerDisplaySettings.ModuleName); } } }