mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
Refactor and modernize codebase for maintainability
Refactored code to improve performance, readability, and scalability: - Removed color temperature constants and obsolete VCP codes. - Converted `MonitorStateManager` methods to async for non-blocking I/O. - Added retry logic for physical monitor discovery in `DdcCiController`. - Simplified UI logic in `MainWindow.xaml.cs` by removing animations. - Streamlined `MainViewModel` initialization and reduced excessive logging. - Enhanced error handling during disposal and initialization processes. - Removed deprecated methods and unused features for cleaner code. - Consolidated repetitive code into reusable helper methods. - Replaced hardcoded UI constants with configurable values in `AppConstants`. These changes align the application with modern coding practices.
This commit is contained in:
@@ -35,11 +35,6 @@ namespace PowerDisplay.Configuration
|
||||
public const int MaxBrightness = 100;
|
||||
public const int DefaultBrightness = 50;
|
||||
|
||||
// Color Temperature (Kelvin)
|
||||
public const int MinColorTemp = 2000; // Warm
|
||||
public const int MaxColorTemp = 10000; // Cool
|
||||
public const int DefaultColorTemp = 6500; // Neutral
|
||||
|
||||
// Contrast
|
||||
public const int MinContrast = 0;
|
||||
public const int MaxContrast = 100;
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace PowerDisplay.Helpers
|
||||
/// <summary>
|
||||
/// Timer callback to save state when dirty
|
||||
/// </summary>
|
||||
private void OnSaveTimerElapsed(object? state)
|
||||
private async void OnSaveTimerElapsed(object? state)
|
||||
{
|
||||
bool shouldSave = false;
|
||||
lock (_lock)
|
||||
@@ -86,7 +86,7 @@ namespace PowerDisplay.Helpers
|
||||
|
||||
if (shouldSave)
|
||||
{
|
||||
SaveStateToDisk();
|
||||
await SaveStateToDiskAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,10 +295,10 @@ namespace PowerDisplay.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current state to disk immediately.
|
||||
/// Save current state to disk immediately (async).
|
||||
/// Called by timer after debounce period or on dispose to flush pending changes.
|
||||
/// </summary>
|
||||
private void SaveStateToDisk()
|
||||
private async Task SaveStateToDiskAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -334,9 +334,9 @@ namespace PowerDisplay.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
// Write to disk
|
||||
// Write to disk asynchronously
|
||||
var json = JsonSerializer.Serialize(stateFile, AppJsonContext.Default.MonitorStateFile);
|
||||
File.WriteAllText(_stateFilePath, json);
|
||||
await File.WriteAllTextAsync(_stateFilePath, json);
|
||||
|
||||
Logger.LogDebug($"[State] Saved state for {stateFile.Monitors.Count} monitors");
|
||||
}
|
||||
@@ -368,7 +368,7 @@ namespace PowerDisplay.Helpers
|
||||
if (wasDirty)
|
||||
{
|
||||
Logger.LogInfo("Flushing pending state changes before dispose");
|
||||
SaveStateToDisk();
|
||||
SaveStateToDiskAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_saveTimer?.Dispose();
|
||||
|
||||
@@ -404,61 +404,12 @@ namespace PowerDisplay.Native.DDC
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sometimes Windows returns NULL handles, so we implement retry logic
|
||||
PHYSICAL_MONITOR[]? physicalMonitors = null;
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 200;
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
if (attempt > 0)
|
||||
{
|
||||
await Task.Delay(retryDelayMs, cancellationToken);
|
||||
}
|
||||
|
||||
physicalMonitors = _discoveryHelper.GetPhysicalMonitors(hMonitor);
|
||||
|
||||
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
||||
{
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null/empty on attempt {attempt + 1}, will retry");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if any handle is NULL
|
||||
bool hasNullHandle = false;
|
||||
for (int i = 0; i < physicalMonitors.Length; i++)
|
||||
{
|
||||
if (physicalMonitors[i].HPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
hasNullHandle = true;
|
||||
Logger.LogWarning($"DDC: Physical monitor [{i}] has NULL handle on attempt {attempt + 1}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNullHandle)
|
||||
{
|
||||
// Success! All handles are valid
|
||||
break;
|
||||
}
|
||||
else if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: NULL handle detected, will retry (attempt {attempt + 1}/{maxRetries})");
|
||||
physicalMonitors = null; // Reset for retry
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"DDC: NULL handle still present after {maxRetries} attempts, continuing anyway");
|
||||
}
|
||||
}
|
||||
// Get physical monitors with retry logic for NULL handle workaround
|
||||
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync(hMonitor, cancellationToken);
|
||||
|
||||
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Failed to get physical monitors for hMonitor 0x{hMonitor:X} after {maxRetries} attempts");
|
||||
Logger.LogWarning($"DDC: Failed to get physical monitors for hMonitor 0x{hMonitor:X} after retries");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -544,6 +495,104 @@ namespace PowerDisplay.Native.DDC
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get physical monitors with retry logic to handle Windows API occasionally returning NULL handles
|
||||
/// </summary>
|
||||
/// <param name="hMonitor">Handle to the monitor</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Array of physical monitors, or null if failed after retries</returns>
|
||||
private async Task<PHYSICAL_MONITOR[]?> GetPhysicalMonitorsWithRetryAsync(
|
||||
IntPtr hMonitor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 200;
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
if (attempt > 0)
|
||||
{
|
||||
await Task.Delay(retryDelayMs, cancellationToken);
|
||||
}
|
||||
|
||||
var monitors = _discoveryHelper.GetPhysicalMonitors(hMonitor);
|
||||
|
||||
var validationResult = ValidatePhysicalMonitors(monitors, attempt, maxRetries);
|
||||
|
||||
if (validationResult.IsValid)
|
||||
{
|
||||
return monitors;
|
||||
}
|
||||
|
||||
if (validationResult.ShouldRetry)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Last attempt failed, return what we have
|
||||
return monitors;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate physical monitors array for null handles
|
||||
/// </summary>
|
||||
/// <returns>Tuple indicating if valid and if should retry</returns>
|
||||
private (bool IsValid, bool ShouldRetry) ValidatePhysicalMonitors(
|
||||
PHYSICAL_MONITOR[]? monitors,
|
||||
int attempt,
|
||||
int maxRetries)
|
||||
{
|
||||
if (monitors == null || monitors.Length == 0)
|
||||
{
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null/empty on attempt {attempt + 1}, will retry");
|
||||
}
|
||||
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
bool hasNullHandle = HasAnyNullHandles(monitors, out int nullIndex);
|
||||
|
||||
if (!hasNullHandle)
|
||||
{
|
||||
return (true, false); // Valid, don't retry
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Physical monitor [{nullIndex}] has NULL handle on attempt {attempt + 1}, will retry");
|
||||
return (false, true); // Invalid, should retry
|
||||
}
|
||||
|
||||
Logger.LogWarning($"DDC: NULL handle still present after {maxRetries} attempts, continuing anyway");
|
||||
return (false, false); // Invalid but no more retries
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if any physical monitor has a NULL handle
|
||||
/// </summary>
|
||||
/// <param name="monitors">Array of physical monitors to check</param>
|
||||
/// <param name="nullIndex">Output index of first NULL handle found, or -1 if none</param>
|
||||
/// <returns>True if any NULL handle found</returns>
|
||||
private bool HasAnyNullHandles(PHYSICAL_MONITOR[] monitors, out int nullIndex)
|
||||
{
|
||||
for (int i = 0; i < monitors.Length; i++)
|
||||
{
|
||||
if (monitors[i].HPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
nullIndex = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
nullIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic method to get VCP feature value
|
||||
/// </summary>
|
||||
@@ -584,7 +633,7 @@ namespace PowerDisplay.Native.DDC
|
||||
value = Math.Clamp(value, min, max);
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
async () =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
@@ -594,7 +643,7 @@ namespace PowerDisplay.Native.DDC
|
||||
try
|
||||
{
|
||||
// Get current value to determine range
|
||||
var currentInfo = GetVcpFeatureAsync(monitor, vcpCode).Result;
|
||||
var currentInfo = await GetVcpFeatureAsync(monitor, vcpCode);
|
||||
if (!currentInfo.IsValid)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Cannot read current value for VCP 0x{vcpCode:X2}");
|
||||
|
||||
@@ -24,20 +24,6 @@ namespace PowerDisplay.Native
|
||||
/// </summary>
|
||||
public const byte VcpCodeContrast = 0x12;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Backlight control (0x13)
|
||||
/// OBSOLETE: PowerDisplay now uses only VcpCodeBrightness (0x10).
|
||||
/// </summary>
|
||||
[Obsolete("Use VcpCodeBrightness (0x10) only. PowerDisplay no longer uses fallback codes.")]
|
||||
public const byte VcpCodeBacklightControl = 0x13;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: White backlight level (0x6B)
|
||||
/// OBSOLETE: PowerDisplay now uses only VcpCodeBrightness (0x10).
|
||||
/// </summary>
|
||||
[Obsolete("Use VcpCodeBrightness (0x10) only. PowerDisplay no longer uses fallback codes.")]
|
||||
public const byte VcpCodeBacklightLevelWhite = 0x6B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio Speaker Volume (0x62)
|
||||
/// Standard VESA MCCS volume control for monitors with built-in speakers.
|
||||
@@ -49,20 +35,6 @@ namespace PowerDisplay.Native
|
||||
/// </summary>
|
||||
public const byte VcpCodeMute = 0x8D;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color Temperature Request (0x0C)
|
||||
/// OBSOLETE: Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.
|
||||
/// </summary>
|
||||
[Obsolete("Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.")]
|
||||
public const byte VcpCodeColorTemperature = 0x0C;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color Temperature Increment (0x0B)
|
||||
/// OBSOLETE: Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.
|
||||
/// </summary>
|
||||
[Obsolete("Not widely supported. Use VcpCodeSelectColorPreset (0x14) instead.")]
|
||||
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Gamma correction (0x72)
|
||||
/// </summary>
|
||||
|
||||
@@ -15,6 +15,7 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using PowerDisplay.Configuration;
|
||||
using PowerDisplay.Core;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
@@ -430,16 +431,10 @@ namespace PowerDisplay
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRefreshClick(object sender, RoutedEventArgs e)
|
||||
private void OnRefreshClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Refresh monitor list
|
||||
if (_viewModel?.RefreshCommand?.CanExecute(null) == true)
|
||||
{
|
||||
@@ -459,16 +454,10 @@ namespace PowerDisplay
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLinkClick(object sender, RoutedEventArgs e)
|
||||
private void OnLinkClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Link all monitor brightness (synchronized adjustment)
|
||||
if (_viewModel != null && _viewModel.Monitors.Count > 0)
|
||||
{
|
||||
@@ -483,16 +472,10 @@ namespace PowerDisplay
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDisableClick(object sender, RoutedEventArgs e)
|
||||
private void OnDisableClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Disable/enable all monitor controls
|
||||
if (_viewModel != null)
|
||||
{
|
||||
@@ -512,72 +495,6 @@ namespace PowerDisplay
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get internal monitor name, consistent with SettingsManager logic
|
||||
/// </summary>
|
||||
private async void OnTestClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ContentDialog? dlg = null;
|
||||
Core.MonitorManager? manager = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Test monitor discovery functionality
|
||||
dlg = new ContentDialog
|
||||
{
|
||||
Title = "Monitor Detection Test",
|
||||
Content = "Starting monitor detection...",
|
||||
CloseButtonText = "Close",
|
||||
XamlRoot = this.Content.XamlRoot,
|
||||
};
|
||||
|
||||
_ = dlg.ShowAsync();
|
||||
|
||||
manager = new Core.MonitorManager();
|
||||
var monitors = await manager.DiscoverMonitorsAsync(default(System.Threading.CancellationToken));
|
||||
|
||||
string message = $"Found {monitors.Count} monitors:\n\n";
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
message += $"• {monitor.Name}\n";
|
||||
message += $" Communication: {monitor.CommunicationMethod}\n";
|
||||
message += $" Brightness: {monitor.CurrentBrightness}%\n\n";
|
||||
}
|
||||
|
||||
if (monitors.Count == 0)
|
||||
{
|
||||
message = "No monitors found.\n\n";
|
||||
message += "Possible reasons:\n";
|
||||
message += "• DDC/CI not supported\n";
|
||||
message += "• Driver issues\n";
|
||||
message += "• Permission issues\n";
|
||||
message += "• Cable doesn't support DDC/CI";
|
||||
}
|
||||
|
||||
dlg.Content = message;
|
||||
|
||||
// Don't dispose manager, use existing manager
|
||||
// Initialize ViewModel and bind to root Grid refresh
|
||||
if (monitors.Count > 0)
|
||||
{
|
||||
// Use existing refresh command
|
||||
await _viewModel.RefreshMonitorsAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"OnTestClick failed: {ex}");
|
||||
if (dlg != null)
|
||||
{
|
||||
dlg.Content = $"Error: {ex.Message}\n\nType: {ex.GetType().Name}";
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure window properties (synchronous, no data dependency)
|
||||
/// </summary>
|
||||
@@ -593,7 +510,7 @@ namespace PowerDisplay
|
||||
if (_appWindow != null)
|
||||
{
|
||||
// Set initial window size - will be adjusted later based on content
|
||||
_appWindow.Resize(new SizeInt32 { Width = 640, Height = 480 });
|
||||
_appWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = 480 });
|
||||
|
||||
// Position window at bottom right corner
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
@@ -692,7 +609,7 @@ namespace PowerDisplay
|
||||
RootGrid.UpdateLayout();
|
||||
|
||||
// Get precise content height
|
||||
var availableWidth = 640.0;
|
||||
var availableWidth = (double)AppConstants.UI.WindowWidth;
|
||||
var contentHeight = GetContentHeight(availableWidth);
|
||||
|
||||
// Account for display scaling
|
||||
@@ -700,16 +617,13 @@ namespace PowerDisplay
|
||||
var scaledHeight = (int)Math.Ceiling(contentHeight * scale);
|
||||
|
||||
// Only set maximum height for scrollable content
|
||||
scaledHeight = Math.Min(scaledHeight, 650);
|
||||
scaledHeight = Math.Min(scaledHeight, AppConstants.UI.MaxWindowHeight);
|
||||
|
||||
// Check if resize is needed
|
||||
var currentSize = _appWindow.Size;
|
||||
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
|
||||
{
|
||||
_appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight });
|
||||
|
||||
// Update clip region to match new window size
|
||||
UpdateClipRegion(640, scaledHeight / scale);
|
||||
_appWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = scaledHeight });
|
||||
|
||||
// Reposition to maintain bottom-right position
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
@@ -735,12 +649,6 @@ namespace PowerDisplay
|
||||
return RootGrid.DesiredSize.Height + 4; // Small padding for fallback method
|
||||
}
|
||||
|
||||
private void UpdateClipRegion(double width, double height)
|
||||
{
|
||||
// Clip region removed to allow automatic sizing
|
||||
// No longer needed as we removed the fixed clip from RootGrid
|
||||
}
|
||||
|
||||
private void PositionWindowAtBottomRight(AppWindow appWindow)
|
||||
{
|
||||
try
|
||||
@@ -754,7 +662,7 @@ namespace PowerDisplay
|
||||
|
||||
// Calculate bottom-right position, close to taskbar
|
||||
// WorkArea already excludes taskbar area, so use WorkArea bottom directly
|
||||
int rightMargin = 10; // Small margin from right edge
|
||||
int rightMargin = AppConstants.UI.WindowRightMargin; // Small margin from right edge
|
||||
int x = workArea.Width - windowSize.Width - rightMargin;
|
||||
int y = workArea.Height - windowSize.Height; // Close to taskbar top, no gap
|
||||
|
||||
@@ -768,17 +676,6 @@ namespace PowerDisplay
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates button press for modern interaction feedback
|
||||
/// </summary>
|
||||
/// <param name="button">The button to animate</param>
|
||||
private async Task AnimateButtonPress(Button button)
|
||||
{
|
||||
// Button animation disabled to avoid compilation errors
|
||||
// Using default button visual states instead
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slider ValueChanged event handler - does nothing during drag
|
||||
/// This allows the slider UI to update smoothly without triggering hardware operations
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
@@ -152,28 +151,18 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
StatusText = "Scanning monitors...";
|
||||
IsScanning = true;
|
||||
Logger.LogInfo("[InitializeAsync] Starting monitor discovery");
|
||||
|
||||
// Discover monitors
|
||||
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
|
||||
Logger.LogInfo($"[InitializeAsync] Discovery completed, found {monitors.Count} monitors");
|
||||
|
||||
// Update UI on the dispatcher thread
|
||||
Logger.LogInfo("[InitializeAsync] Calling TryEnqueue to update UI");
|
||||
bool enqueued = _dispatcherQueue.TryEnqueue(() =>
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
Logger.LogInfo("[InitializeAsync] TryEnqueue lambda started");
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[InitializeAsync] Calling UpdateMonitorList");
|
||||
UpdateMonitorList(monitors);
|
||||
Logger.LogInfo("[InitializeAsync] UpdateMonitorList returned");
|
||||
|
||||
IsScanning = false;
|
||||
Logger.LogInfo("[InitializeAsync] IsScanning set to false");
|
||||
|
||||
IsInitialized = true;
|
||||
Logger.LogInfo("[InitializeAsync] IsInitialized set to true");
|
||||
|
||||
if (monitors.Count > 0)
|
||||
{
|
||||
@@ -183,23 +172,18 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
StatusText = "No controllable monitors found";
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[InitializeAsync] StatusText set to: {StatusText}");
|
||||
Logger.LogInfo("[InitializeAsync] TryEnqueue lambda completed successfully");
|
||||
}
|
||||
catch (Exception lambdaEx)
|
||||
{
|
||||
Logger.LogError($"[InitializeAsync] Exception in TryEnqueue lambda: {lambdaEx.Message}");
|
||||
Logger.LogError($"[InitializeAsync] Stack trace: {lambdaEx.StackTrace}");
|
||||
Logger.LogError($"[InitializeAsync] UI update failed: {lambdaEx.Message}");
|
||||
IsScanning = false;
|
||||
throw;
|
||||
StatusText = $"UI update failed: {lambdaEx.Message}";
|
||||
}
|
||||
});
|
||||
Logger.LogInfo($"[InitializeAsync] TryEnqueue returned: {enqueued}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[InitializeAsync] Exception in InitializeAsync: {ex.Message}");
|
||||
Logger.LogError($"[InitializeAsync] Monitor discovery failed: {ex.Message}");
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
StatusText = $"Scan failed: {ex.Message}";
|
||||
@@ -241,10 +225,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
private void UpdateMonitorList(IReadOnlyList<Monitor> monitors)
|
||||
{
|
||||
Logger.LogInfo($"[UpdateMonitorList] Starting with {monitors.Count} monitors");
|
||||
|
||||
Monitors.Clear();
|
||||
Logger.LogInfo("[UpdateMonitorList] Cleared Monitors collection");
|
||||
|
||||
// Load settings to check for hidden monitors
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
|
||||
@@ -252,67 +233,45 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
settings.Properties.Monitors
|
||||
.Where(m => m.IsHidden)
|
||||
.Select(m => m.HardwareId));
|
||||
Logger.LogInfo($"[UpdateMonitorList] Loaded settings, found {hiddenMonitorIds.Count} hidden monitor IDs");
|
||||
|
||||
var colorTempTasks = new List<Task>();
|
||||
Logger.LogInfo("[UpdateMonitorList] Starting monitor loop");
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
Logger.LogInfo($"[UpdateMonitorList] Processing monitor: {monitor.Name} (HardwareId: {monitor.HardwareId})");
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[UpdateMonitorList] Creating MonitorViewModel for {monitor.Name}");
|
||||
var vm = new MonitorViewModel(monitor, _monitorManager, this);
|
||||
Monitors.Add(vm);
|
||||
Logger.LogInfo($"[UpdateMonitorList] Added MonitorViewModel, total count: {Monitors.Count}");
|
||||
|
||||
// Asynchronously initialize color temperature for DDC/CI monitors
|
||||
if (monitor.SupportsColorTemperature && monitor.CommunicationMethod == "DDC/CI")
|
||||
{
|
||||
Logger.LogInfo($"[UpdateMonitorList] Initializing color temperature for {monitor.Name}");
|
||||
var task = InitializeColorTemperatureSafeAsync(monitor.Id, vm);
|
||||
colorTempTasks.Add(task);
|
||||
Logger.LogInfo($"[UpdateMonitorList] Color temp task added, total tasks: {colorTempTasks.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInfo("[UpdateMonitorList] Monitor loop completed");
|
||||
|
||||
Logger.LogInfo("[UpdateMonitorList] Calling OnPropertyChanged(HasMonitors)");
|
||||
OnPropertyChanged(nameof(HasMonitors));
|
||||
Logger.LogInfo("[UpdateMonitorList] Calling OnPropertyChanged(ShowNoMonitorsMessage)");
|
||||
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
|
||||
Logger.LogInfo("[UpdateMonitorList] OnPropertyChanged calls completed");
|
||||
|
||||
// Wait for color temperature initialization to complete before saving
|
||||
// This ensures we save the actual scanned values instead of defaults
|
||||
if (colorTempTasks.Count > 0)
|
||||
{
|
||||
Logger.LogInfo($"[UpdateMonitorList] Waiting for {colorTempTasks.Count} color temperature tasks to complete before saving...");
|
||||
|
||||
// Use fire-and-forget async method to avoid blocking UI thread
|
||||
_ = WaitForColorTempAndSaveAsync(colorTempTasks);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No color temperature tasks, save immediately
|
||||
Logger.LogInfo("[UpdateMonitorList] No color temperature tasks, calling SaveMonitorsToSettings immediately");
|
||||
SaveMonitorsToSettings();
|
||||
Logger.LogInfo("[UpdateMonitorList] SaveMonitorsToSettings completed");
|
||||
|
||||
// Restore saved settings if enabled (async, don't block)
|
||||
Logger.LogInfo("[UpdateMonitorList] About to call ReloadMonitorSettingsAsync");
|
||||
_ = ReloadMonitorSettingsAsync(null);
|
||||
Logger.LogInfo("[UpdateMonitorList] ReloadMonitorSettingsAsync invoked (fire-and-forget)");
|
||||
}
|
||||
|
||||
Logger.LogInfo("[UpdateMonitorList] Method returning");
|
||||
}
|
||||
|
||||
private async Task WaitForColorTempAndSaveAsync(List<Task> colorTempTasks)
|
||||
@@ -321,22 +280,17 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
// Wait for all color temperature initialization tasks to complete
|
||||
await Task.WhenAll(colorTempTasks);
|
||||
Logger.LogInfo("[WaitForColorTempAndSaveAsync] Color temperature tasks completed");
|
||||
|
||||
// Save monitor information to settings.json and reload settings
|
||||
// Must be done on UI thread since these methods access UI properties and observable collections
|
||||
Logger.LogInfo("[WaitForColorTempAndSaveAsync] Dispatching save and reload to UI thread");
|
||||
_dispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
SaveMonitorsToSettings();
|
||||
Logger.LogInfo("[WaitForColorTempAndSaveAsync] SaveMonitorsToSettings completed with actual color temp values");
|
||||
|
||||
// Restore saved settings if enabled (async)
|
||||
Logger.LogInfo("[WaitForColorTempAndSaveAsync] About to call ReloadMonitorSettingsAsync");
|
||||
await ReloadMonitorSettingsAsync(null); // Tasks already completed, pass null
|
||||
Logger.LogInfo("[WaitForColorTempAndSaveAsync] ReloadMonitorSettingsAsync completed");
|
||||
}
|
||||
catch (Exception innerEx)
|
||||
{
|
||||
@@ -352,7 +306,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
SaveMonitorsToSettings();
|
||||
Logger.LogInfo("[WaitForColorTempAndSaveAsync] SaveMonitorsToSettings completed (after error)");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -605,37 +558,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
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>
|
||||
@@ -653,14 +575,9 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[ReloadMonitorSettingsAsync] Setting IsLoading = true");
|
||||
|
||||
// Set loading state to block UI interactions
|
||||
IsLoading = true;
|
||||
Logger.LogInfo("[ReloadMonitorSettingsAsync] IsLoading set to true");
|
||||
|
||||
StatusText = "Loading settings...";
|
||||
Logger.LogInfo("[ReloadMonitorSettingsAsync] StatusText updated");
|
||||
|
||||
// Read current settings
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
@@ -668,19 +585,14 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
if (settings.Properties.RestoreSettingsOnStartup)
|
||||
{
|
||||
// Restore saved settings from configuration file
|
||||
Logger.LogInfo("[Startup] RestoreSettingsOnStartup enabled - applying saved settings");
|
||||
|
||||
foreach (var monitorVm in Monitors)
|
||||
{
|
||||
var hardwareId = monitorVm.HardwareId;
|
||||
Logger.LogInfo($"[Startup] Processing monitor: '{monitorVm.Name}', HardwareId: '{hardwareId}'");
|
||||
|
||||
// Find and apply corresponding saved settings from state file using stable HardwareId
|
||||
var savedState = _stateManager.GetMonitorParameters(hardwareId);
|
||||
if (savedState.HasValue)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Restoring state for HardwareId '{hardwareId}': Brightness={savedState.Value.Brightness}, ColorTemp={savedState.Value.ColorTemperature}");
|
||||
|
||||
// Validate and apply saved values (skip invalid values)
|
||||
// Use UpdatePropertySilently to avoid triggering hardware updates during initialization
|
||||
if (savedState.Value.Brightness >= monitorVm.MinBrightness && savedState.Value.Brightness <= monitorVm.MaxBrightness)
|
||||
@@ -689,7 +601,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}', skipping");
|
||||
Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}'");
|
||||
}
|
||||
|
||||
// Color temperature: VCP 0x14 preset value (discrete values, no range check needed)
|
||||
@@ -707,10 +619,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Contrast), savedState.Value.Contrast);
|
||||
}
|
||||
else if (!monitorVm.ShowContrast)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Contrast not supported on HardwareId '{hardwareId}', skipping");
|
||||
}
|
||||
|
||||
// Volume validation - only apply if hardware supports it
|
||||
if (monitorVm.ShowVolume &&
|
||||
@@ -719,14 +627,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Volume), savedState.Value.Volume);
|
||||
}
|
||||
else if (!monitorVm.ShowVolume)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Volume not supported on HardwareId '{hardwareId}', skipping");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"[Startup] No saved state for HardwareId '{hardwareId}' - keeping current hardware values");
|
||||
}
|
||||
|
||||
// Apply feature visibility settings using HardwareId
|
||||
@@ -738,27 +638,18 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
else
|
||||
{
|
||||
// Save current hardware values to configuration file
|
||||
Logger.LogInfo("[Startup] RestoreSettingsOnStartup disabled - saving current hardware values");
|
||||
|
||||
// Wait for color temperature initialization to complete (if any)
|
||||
if (colorTempInitTasks != null && colorTempInitTasks.Count > 0)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Waiting for {colorTempInitTasks.Count} color temperature initialization tasks to complete...");
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[Startup] Calling Task.WhenAll on color temp tasks");
|
||||
await Task.WhenAll(colorTempInitTasks);
|
||||
Logger.LogInfo("[Startup] Task.WhenAll completed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[Startup] Some color temperature initializations failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("[Startup] No color temperature tasks to wait for");
|
||||
}
|
||||
|
||||
foreach (var monitorVm in Monitors)
|
||||
{
|
||||
@@ -768,8 +659,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
SaveMonitorSettingDirect(monitorVm.HardwareId, "Contrast", monitorVm.Contrast);
|
||||
SaveMonitorSettingDirect(monitorVm.HardwareId, "Volume", monitorVm.Volume);
|
||||
|
||||
Logger.LogInfo($"[Startup] Saved current values for Hardware ID '{monitorVm.HardwareId}': Brightness={monitorVm.Brightness}, ColorTemp={monitorVm.ColorTemperature}");
|
||||
|
||||
// Apply feature visibility settings
|
||||
ApplyFeatureVisibility(monitorVm, settings);
|
||||
}
|
||||
@@ -780,7 +669,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[ReloadMonitorSettingsAsync] Exception caught: {ex.Message}");
|
||||
Logger.LogError($"[ReloadMonitorSettingsAsync] Failed to reload settings: {ex.Message}");
|
||||
Logger.LogError($"[ReloadMonitorSettingsAsync] Stack trace: {ex.StackTrace}");
|
||||
StatusText = $"Failed to process settings: {ex.Message}";
|
||||
}
|
||||
@@ -881,34 +770,8 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
foreach (var vm in Monitors)
|
||||
{
|
||||
var monitorInfo = new Microsoft.PowerToys.Settings.UI.Library.MonitorInfo(
|
||||
name: vm.Name,
|
||||
internalName: vm.Id,
|
||||
hardwareId: vm.HardwareId,
|
||||
communicationMethod: vm.CommunicationMethod,
|
||||
monitorType: vm.IsInternal ? "Internal" : "External",
|
||||
currentBrightness: vm.Brightness,
|
||||
colorTemperature: vm.ColorTemperature)
|
||||
{
|
||||
CapabilitiesRaw = vm.CapabilitiesRaw,
|
||||
VcpCodes = vm.VcpCapabilitiesInfo?.SupportedVcpCodes
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => $"0x{kvp.Key:X2}")
|
||||
.ToList() ?? new List<string>(),
|
||||
VcpCodesFormatted = vm.VcpCapabilitiesInfo?.SupportedVcpCodes
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => FormatVcpCodeForDisplay(kvp.Key, kvp.Value))
|
||||
.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.EnableContrast = existingMonitor.EnableContrast;
|
||||
monitorInfo.EnableVolume = existingMonitor.EnableVolume;
|
||||
}
|
||||
|
||||
var monitorInfo = CreateMonitorInfo(vm);
|
||||
ApplyPreservedUserSettings(monitorInfo, existingMonitorSettings);
|
||||
monitors.Add(monitorInfo);
|
||||
}
|
||||
|
||||
@@ -942,6 +805,63 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create MonitorInfo object from MonitorViewModel
|
||||
/// </summary>
|
||||
private Microsoft.PowerToys.Settings.UI.Library.MonitorInfo CreateMonitorInfo(MonitorViewModel vm)
|
||||
{
|
||||
return new Microsoft.PowerToys.Settings.UI.Library.MonitorInfo(
|
||||
name: vm.Name,
|
||||
internalName: vm.Id,
|
||||
hardwareId: vm.HardwareId,
|
||||
communicationMethod: vm.CommunicationMethod,
|
||||
monitorType: vm.IsInternal ? "Internal" : "External",
|
||||
currentBrightness: vm.Brightness,
|
||||
colorTemperature: vm.ColorTemperature)
|
||||
{
|
||||
CapabilitiesRaw = vm.CapabilitiesRaw,
|
||||
VcpCodes = BuildVcpCodesList(vm),
|
||||
VcpCodesFormatted = BuildFormattedVcpCodesList(vm),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build list of VCP codes in hex format
|
||||
/// </summary>
|
||||
private List<string> BuildVcpCodesList(MonitorViewModel vm)
|
||||
{
|
||||
return vm.VcpCapabilitiesInfo?.SupportedVcpCodes
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => $"0x{kvp.Key:X2}")
|
||||
.ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build list of formatted VCP codes with display info
|
||||
/// </summary>
|
||||
private List<Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo> BuildFormattedVcpCodesList(MonitorViewModel vm)
|
||||
{
|
||||
return vm.VcpCapabilitiesInfo?.SupportedVcpCodes
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => FormatVcpCodeForDisplay(kvp.Key, kvp.Value))
|
||||
.ToList() ?? new List<Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply preserved user settings from existing monitor settings
|
||||
/// </summary>
|
||||
private void ApplyPreservedUserSettings(
|
||||
Microsoft.PowerToys.Settings.UI.Library.MonitorInfo monitorInfo,
|
||||
Dictionary<string, Microsoft.PowerToys.Settings.UI.Library.MonitorInfo> existingSettings)
|
||||
{
|
||||
if (existingSettings.TryGetValue(monitorInfo.HardwareId, out var existingMonitor))
|
||||
{
|
||||
monitorInfo.IsHidden = existingMonitor.IsHidden;
|
||||
monitorInfo.EnableContrast = existingMonitor.EnableContrast;
|
||||
monitorInfo.EnableVolume = existingMonitor.EnableVolume;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal Settings UI that the monitor list has been refreshed
|
||||
/// </summary>
|
||||
@@ -1018,18 +938,25 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
// State is already persisted, no pending changes to wait for.
|
||||
|
||||
// Quick cleanup of monitor view models
|
||||
try
|
||||
foreach (var vm in Monitors)
|
||||
{
|
||||
foreach (var vm in Monitors)
|
||||
try
|
||||
{
|
||||
vm?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug($"Error disposing monitor VM: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Monitors.Clear();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
Logger.LogDebug($"Error clearing Monitors collection: {ex.Message}");
|
||||
}
|
||||
|
||||
// Release monitor manager
|
||||
@@ -1037,9 +964,9 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
_monitorManager?.Dispose();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
Logger.LogDebug($"Error disposing MonitorManager: {ex.Message}");
|
||||
}
|
||||
|
||||
// Release state manager
|
||||
@@ -1047,9 +974,9 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
_stateManager?.Dispose();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
Logger.LogDebug($"Error disposing StateManager: {ex.Message}");
|
||||
}
|
||||
|
||||
// Finally release cancellation token
|
||||
@@ -1057,9 +984,9 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
Logger.LogDebug($"Error disposing CancellationTokenSource: {ex.Message}");
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
Reference in New Issue
Block a user