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 @@
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);
}
}
}