mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
Refactor PowerDisplay for dynamic monitor capabilities
Removed reliance on static `MonitorType` enumeration, replacing it with dynamic `CommunicationMethod` for better flexibility. Updated `IMonitorController` and `MonitorManager` to dynamically determine monitor control capabilities. Refactored `Monitor` model to streamline properties and improve color temperature handling. Enhanced `MonitorViewModel` with unified methods for brightness, contrast, volume, and color temperature updates, improving UI responsiveness and hardware synchronization. Improved settings handling by adding support for hidden monitors, preserving user preferences, and separating UI configuration from hardware parameter updates. Updated the PowerDisplay Settings UI with warnings, confirmation dialogs, and better VCP capabilities formatting. Removed legacy IPC code in favor of event-driven settings updates. Conducted general code cleanup, improving logging, error handling, and documentation for maintainability.
This commit is contained in:
@@ -21,11 +21,6 @@ namespace PowerDisplay.Core.Interfaces
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported monitor type
|
||||
/// </summary>
|
||||
MonitorType SupportedType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified monitor can be controlled
|
||||
/// </summary>
|
||||
|
||||
@@ -35,11 +35,6 @@ namespace PowerDisplay.Core.Models
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor type
|
||||
/// </summary>
|
||||
public MonitorType Type { get; set; } = MonitorType.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Current brightness (0-100)
|
||||
/// </summary>
|
||||
@@ -87,10 +82,10 @@ namespace PowerDisplay.Core.Models
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable color temperature preset name (e.g., "6500K", "sRGB")
|
||||
/// Human-readable color temperature preset name (e.g., "6500K (0x05)", "sRGB (0x01)")
|
||||
/// </summary>
|
||||
public string ColorTemperaturePresetName =>
|
||||
VcpValueNames.GetName(0x14, CurrentColorTemperature) ?? $"0x{CurrentColorTemperature:X2}";
|
||||
VcpValueNames.GetFormattedName(0x14, CurrentColorTemperature);
|
||||
|
||||
/// <summary>
|
||||
/// 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}%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor type enumeration
|
||||
/// </summary>
|
||||
public enum MonitorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown type
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Internal display (laptop screen, controlled via WMI)
|
||||
/// </summary>
|
||||
Internal,
|
||||
|
||||
/// <summary>
|
||||
/// External display (controlled via DDC/CI)
|
||||
/// </summary>
|
||||
External,
|
||||
|
||||
/// <summary>
|
||||
/// HDR display (controlled via Display Config API)
|
||||
/// </summary>
|
||||
HDR,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor capability flags based on parsed VCP capabilities
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
@@ -158,17 +158,34 @@ namespace PowerDisplay.Core.Utils
|
||||
/// </summary>
|
||||
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
|
||||
/// <param name="value">Value to translate</param>
|
||||
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
|
||||
public static string GetName(byte vcpCode, int value)
|
||||
/// <returns>Name string like "sRGB" or null if unknown</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get formatted display name for a VCP value (with hex value in parentheses)
|
||||
/// </summary>
|
||||
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
|
||||
/// <param name="value">Value to translate</param>
|
||||
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
|
||||
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}";
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified monitor can be controlled
|
||||
/// </summary>
|
||||
public async Task<bool> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -30,14 +30,12 @@ namespace PowerDisplay.Native.WMI
|
||||
|
||||
public string Name => "WMI Monitor Controller (WmiLight)";
|
||||
|
||||
public MonitorType SupportedType => MonitorType.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified monitor can be controlled
|
||||
/// </summary>
|
||||
public async Task<bool> 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,
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace PowerDisplay
|
||||
{
|
||||
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
|
||||
{
|
||||
_ = mainWindow.ViewModel.ReloadMonitorSettingsAsync();
|
||||
mainWindow.ViewModel.ApplySettingsFromUI();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace PowerDisplay.Serialization
|
||||
/// JSON source generation context for AOT compatibility.
|
||||
/// Eliminates reflection-based JSON serialization.
|
||||
/// </summary>
|
||||
[JsonSerializable(typeof(PowerDisplayMonitorsIPCResponse))]
|
||||
[JsonSerializable(typeof(MonitorInfoData))]
|
||||
[JsonSerializable(typeof(IPCMessageAction))]
|
||||
[JsonSerializable(typeof(MonitorStateFile))]
|
||||
|
||||
@@ -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<MonitorViewModel> _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>(PowerDisplaySettings.ModuleName);
|
||||
var hiddenMonitorIds = new HashSet<string>(
|
||||
settings.Properties.Monitors
|
||||
.Where(m => m.IsHidden)
|
||||
.Select(m => m.HardwareId));
|
||||
|
||||
var colorTempTasks = new List<Task>();
|
||||
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>(PowerDisplaySettings.ModuleName);
|
||||
var hiddenMonitorIds = new HashSet<string>(
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup settings file watcher
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle settings file changes - only monitors UI configuration changes from Settings UI
|
||||
/// (monitor_state.json is managed separately and doesn't trigger this)
|
||||
/// </summary>
|
||||
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<PowerDisplaySettings>("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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe wrapper for initializing color temperature asynchronously
|
||||
/// </summary>
|
||||
@@ -450,7 +371,173 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public async void ApplySettingsFromUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[Settings] Processing settings update from Settings UI");
|
||||
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply UI-only configuration changes (feature visibility toggles)
|
||||
/// Synchronous, lightweight operation
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
private async Task ApplyHardwareParametersAsync(PowerDisplaySettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[Settings] Applying hardware parameter changes");
|
||||
|
||||
var updateTasks = new List<Task>();
|
||||
|
||||
foreach (var monitorVm in Monitors)
|
||||
{
|
||||
var hardwareId = monitorVm.HardwareId;
|
||||
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m => m.HardwareId == hardwareId);
|
||||
|
||||
if (monitorSettings == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply brightness to a specific monitor
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply color temperature to a specific monitor
|
||||
/// </summary>
|
||||
private async Task ApplyColorTemperatureAsync(MonitorViewModel monitorVm, int colorTemperature)
|
||||
{
|
||||
// Use MonitorViewModel's unified method
|
||||
await monitorVm.SetColorTemperatureAsync(colorTemperature);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply Settings UI configuration changes (feature visibility toggles only)
|
||||
/// OBSOLETE: Use ApplySettingsFromUI() instead
|
||||
/// </summary>
|
||||
[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<PowerDisplaySettings>("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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload monitor settings from configuration - ONLY called at startup
|
||||
/// </summary>
|
||||
/// <param name="colorTempInitTasks">Optional tasks for color temperature initialization to wait for</param>
|
||||
public async Task ReloadMonitorSettingsAsync(List<Task>? 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>(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<Microsoft.PowerToys.Settings.UI.Library.MonitorInfo>();
|
||||
@@ -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<Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo>(),
|
||||
};
|
||||
|
||||
// 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>(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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal Settings UI that the monitor list has been refreshed
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format VCP code information for display in Settings UI
|
||||
/// </summary>
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply brightness with hardware update and state persistence.
|
||||
/// Can be called from Flyout UI (with debounce) or Settings UI/IPC (immediate).
|
||||
/// </summary>
|
||||
/// <param name="brightness">Brightness value (0-100)</param>
|
||||
/// <param name="immediate">If true, applies immediately; if false, debounces for smooth slider</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply contrast with hardware update and state persistence.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply volume with hardware update and state persistence.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply color temperature with hardware update and state persistence.
|
||||
/// Always immediate (no debouncing for discrete preset values).
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method - applies brightness to hardware and persists state.
|
||||
/// Unified logic for all sources (Flyout, Settings, etc.).
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method - applies contrast to hardware and persists state.
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method - applies volume to hardware and persists state.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
public int ColorTemperature => _monitor.CurrentColorTemperature;
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable color temperature preset name (e.g., "6500K", "sRGB")
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -387,6 +387,41 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonIgnore]
|
||||
public string VolumeTooltip => _supportsVolume ? string.Empty : "Volume control not supported by this monitor";
|
||||
|
||||
/// <summary>
|
||||
/// Generate formatted text of all VCP codes for clipboard
|
||||
/// </summary>
|
||||
public string GetVcpCodesAsText()
|
||||
{
|
||||
if (_vcpCodesFormatted == null || _vcpCodesFormatted.Count == 0)
|
||||
{
|
||||
return "No VCP codes detected";
|
||||
}
|
||||
|
||||
var lines = new List<string>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a color temperature preset item for VCP code 0x14
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// IPC Response message for PowerDisplay monitors information
|
||||
/// </summary>
|
||||
public class PowerDisplayMonitorsIPCResponse
|
||||
{
|
||||
[JsonPropertyName("response_type")]
|
||||
public string ResponseType { get; set; } = "powerdisplay_monitors";
|
||||
|
||||
[JsonPropertyName("monitors")]
|
||||
public List<MonitorInfoData> Monitors { get; set; } = new List<MonitorInfoData>();
|
||||
|
||||
public PowerDisplayMonitorsIPCResponse(List<MonitorInfoData> monitors)
|
||||
{
|
||||
Monitors = monitors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))]
|
||||
|
||||
@@ -23,8 +23,6 @@ namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
|
||||
public static event EventHandler<AllHotkeyConflictsEventArgs> AllHotkeyConflictsReceived;
|
||||
|
||||
public static event EventHandler<MonitorInfo[]> 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<PowerDisplayMonitorsIPCResponse>(jsonString);
|
||||
|
||||
if (response?.Monitors == null)
|
||||
{
|
||||
PowerDisplayMonitorsReceived?.Invoke(this, Array.Empty<MonitorInfo>());
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,21 +123,36 @@
|
||||
x:Uid="PowerDisplay_Monitor_ColorTemperature"
|
||||
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard.Description>
|
||||
<StackPanel Spacing="4">
|
||||
<!-- Simple warning message when supported -->
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{x:Bind SupportsColorTemperature, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Run Text="⚠" FontWeight="SemiBold" />
|
||||
<Run Text=" " />
|
||||
<Run Text="Warning: Modifying this setting may cause unpredictable results. Change only if you understand what it does." />
|
||||
</TextBlock>
|
||||
<!-- Not supported message -->
|
||||
<TextBlock
|
||||
Text="{x:Bind ColorTemperatureTooltip, Mode=OneWay}"
|
||||
Visibility="{x:Bind SupportsColorTemperature, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard.Description>
|
||||
<ComboBox
|
||||
x:Name="ColorTemperatureComboBox"
|
||||
MinWidth="200"
|
||||
ItemsSource="{x:Bind AvailableColorPresets, Mode=OneWay}"
|
||||
SelectedValue="{x:Bind ColorTemperature, Mode=TwoWay}"
|
||||
SelectedValue="{x:Bind ColorTemperature, Mode=OneWay}"
|
||||
SelectedValuePath="VcpValue"
|
||||
DisplayMemberPath="DisplayName"
|
||||
PlaceholderText="Not available"
|
||||
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
|
||||
IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}"
|
||||
SelectionChanged="ColorTemperatureComboBox_SelectionChanged"
|
||||
Tag="{x:Bind}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{x:Bind ColorTemperatureTooltip, Mode=OneWay}" />
|
||||
<TextBlock Text="Changing this setting requires confirmation" />
|
||||
</ToolTipService.ToolTip>
|
||||
</ComboBox>
|
||||
</tkcontrols:SettingsCard>
|
||||
@@ -201,9 +216,8 @@
|
||||
|
||||
<!-- VCP Capabilities -->
|
||||
<tkcontrols:SettingsCard
|
||||
Header="VCP Capabilities (Debug Info)"
|
||||
Description="DDC/CI VCP codes and supported values"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
Header="VCP Capabilities"
|
||||
Description="DDC/CI VCP codes and supported values (for debugging purposes)"
|
||||
Visibility="{x:Bind HasCapabilities, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Button
|
||||
Content=""
|
||||
@@ -214,16 +228,54 @@
|
||||
</ToolTipService.ToolTip>
|
||||
<Button.Flyout>
|
||||
<Flyout ShouldConstrainToRootBounds="False">
|
||||
<ScrollViewer MaxHeight="500" Width="450" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Detected VCP Codes" FontWeight="SemiBold" FontSize="13" />
|
||||
<ItemsControl ItemsSource="{x:Bind VcpCodesFormatted, Mode=OneWay}">
|
||||
<Grid Width="550">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header with Copy Button -->
|
||||
<Grid Grid.Row="0" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Detected VCP Codes"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Padding="8,4"
|
||||
Click="CopyVcpCodes_Click"
|
||||
Tag="{x:Bind}"
|
||||
ToolTipService.ToolTip="Copy all VCP codes to clipboard">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<FontIcon Glyph="" FontSize="12" />
|
||||
<TextBlock Text="Copy" FontSize="12" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- VCP Codes List -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
MaxHeight="500"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
HorizontalScrollMode="Disabled">
|
||||
<ItemsControl ItemsSource="{x:Bind VcpCodesFormatted, Mode=OneWay}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="library:VcpCodeDisplayInfo">
|
||||
<StackPanel Orientation="Vertical" Margin="0,4">
|
||||
<StackPanel Orientation="Vertical"
|
||||
Margin="0,4"
|
||||
HorizontalAlignment="Stretch">
|
||||
<TextBlock Text="{x:Bind Title}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="11" />
|
||||
FontSize="11"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Text="{x:Bind Values}"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
@@ -234,8 +286,8 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
@@ -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<string, int> _previousColorTemperatureValues = new Dictionary<string, int>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MonitorInfo>(_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<MonitorInfo>(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<MonitorInfo>());
|
||||
@@ -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<MonitorInfo.ColorPresetItem>(presetList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format color temperature display name for Settings UI
|
||||
/// Examples:
|
||||
/// - Undefined values: "Manufacturer Defined (0x05)"
|
||||
/// - Predefined values: "6500K (0x05)", "sRGB (0x01)"
|
||||
/// </summary>
|
||||
private string FormatColorTemperatureDisplayName(string name, int vcpValue)
|
||||
{
|
||||
var hexValue = $"0x{vcpValue:X2}";
|
||||
|
||||
// Check if name is undefined (null or empty)
|
||||
// GetName now returns null for unknown values instead of hex string
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return $"Manufacturer Defined ({hexValue})";
|
||||
}
|
||||
|
||||
// For predefined names, append the hex value in parentheses
|
||||
// Examples: "6500K (0x05)", "sRGB (0x01)"
|
||||
return $"{name} ({hexValue})";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -397,6 +429,42 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
SendConfigMSG(JsonSerializer.Serialize(actionMessage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload monitor list from settings file (called when PowerDisplay.exe signals monitor changes)
|
||||
/// </summary>
|
||||
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>(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<MonitorInfo>(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<string, int> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user