mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Refactor and optimize profile and monitor handling
Refactored profile name generation by centralizing logic in `ProfileHelper` with overloads for flexibility. Simplified folder creation logic in `PathConstants` using a reusable helper method. Improved profile loading and saving in `ProfileService` with internal helper methods for better error handling and reduced duplication. Optimized monitor key generation and lookup with concise expressions and dictionary-based retrieval for O(1) performance. Introduced caching for color presets in `MonitorInfo` to avoid redundant computations and added a helper for range validation in `MainViewModel.Settings`. Centralized percentage formatting and property change handling to reduce duplication. Removed redundant methods in `PowerDisplayViewModel` and streamlined event unsubscription in `MainWindow`. Enhanced logging, readability, and maintainability across the codebase.
This commit is contained in:
@@ -77,24 +77,6 @@ namespace PowerDisplay.Common.Models
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the next available profile name (Profile1, Profile2, etc.)
|
||||
/// </summary>
|
||||
public string GenerateProfileName()
|
||||
{
|
||||
int counter = 1;
|
||||
while (true)
|
||||
{
|
||||
string name = $"Profile{counter}";
|
||||
if (GetProfile(name) == null)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a profile name is valid and available
|
||||
/// </summary>
|
||||
|
||||
@@ -86,23 +86,22 @@ namespace PowerDisplay.Common
|
||||
/// </summary>
|
||||
/// <returns>The PowerDisplay folder path</returns>
|
||||
public static string EnsurePowerDisplayFolderExists()
|
||||
{
|
||||
var folderPath = PowerDisplayFolderPath;
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
|
||||
return folderPath;
|
||||
}
|
||||
=> EnsureFolderExists(PowerDisplayFolderPath);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the LightSwitch folder exists. Creates it if necessary.
|
||||
/// </summary>
|
||||
/// <returns>The LightSwitch folder path</returns>
|
||||
public static string EnsureLightSwitchFolderExists()
|
||||
=> EnsureFolderExists(LightSwitchFolderPath);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the specified folder exists. Creates it if necessary.
|
||||
/// </summary>
|
||||
/// <param name="folderPath">The folder path to ensure exists</param>
|
||||
/// <returns>The folder path</returns>
|
||||
private static string EnsureFolderExists(string folderPath)
|
||||
{
|
||||
var folderPath = LightSwitchFolderPath;
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.CreateDirectory(folderPath);
|
||||
|
||||
@@ -46,39 +46,13 @@ namespace PowerDisplay.Common.Services
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
var (profiles, message) = LoadProfilesInternal();
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
var filePath = PathConstants.ProfilesFilePath;
|
||||
|
||||
// Ensure directory exists
|
||||
PathConstants.EnsurePowerDisplayFolderExists();
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var profiles = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.PowerDisplayProfiles);
|
||||
|
||||
if (profiles != null)
|
||||
{
|
||||
// Clean up any legacy Custom profiles
|
||||
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
Logger.LogInfo($"{LogPrefix} Loaded {profiles.Profiles.Count} profiles from {filePath}");
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"{LogPrefix} No profiles file found at {filePath}, returning empty collection");
|
||||
}
|
||||
|
||||
return new PowerDisplayProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{LogPrefix} Failed to load profiles: {ex.Message}");
|
||||
return new PowerDisplayProfiles();
|
||||
Logger.LogInfo($"{LogPrefix} {message}");
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,35 +66,19 @@ namespace PowerDisplay.Common.Services
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
if (profiles == null)
|
||||
{
|
||||
if (profiles == null)
|
||||
{
|
||||
Logger.LogWarning($"{LogPrefix} Cannot save null profiles");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
PathConstants.EnsurePowerDisplayFolderExists();
|
||||
|
||||
// Clean up any Custom profiles before saving
|
||||
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
profiles.LastUpdated = DateTime.UtcNow;
|
||||
|
||||
var json = JsonSerializer.Serialize(profiles, ProfileSerializationContext.Default.PowerDisplayProfiles);
|
||||
|
||||
var filePath = PathConstants.ProfilesFilePath;
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
Logger.LogInfo($"{LogPrefix} Saved {profiles.Profiles.Count} profiles to {filePath}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{LogPrefix} Failed to save profiles: {ex.Message}");
|
||||
Logger.LogWarning($"{LogPrefix} Cannot save null profiles");
|
||||
return false;
|
||||
}
|
||||
|
||||
var (success, message) = SaveProfilesInternal(profiles);
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
Logger.LogInfo($"{LogPrefix} {message}");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,17 +98,17 @@ namespace PowerDisplay.Common.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
var profiles = LoadProfilesInternal();
|
||||
var (profiles, _) = LoadProfilesInternal();
|
||||
profiles.SetProfile(profile);
|
||||
|
||||
var result = SaveProfilesInternal(profiles);
|
||||
var (success, _) = SaveProfilesInternal(profiles);
|
||||
|
||||
if (result)
|
||||
if (success)
|
||||
{
|
||||
Logger.LogInfo($"{LogPrefix} Profile '{profile.Name}' added/updated with {profile.MonitorSettings.Count} monitors");
|
||||
}
|
||||
|
||||
return result;
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +122,7 @@ namespace PowerDisplay.Common.Services
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var profiles = LoadProfilesInternal();
|
||||
var (profiles, _) = LoadProfilesInternal();
|
||||
bool removed = profiles.RemoveProfile(profileName);
|
||||
|
||||
if (removed)
|
||||
@@ -191,7 +149,7 @@ namespace PowerDisplay.Common.Services
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var profiles = LoadProfilesInternal();
|
||||
var (profiles, _) = LoadProfilesInternal();
|
||||
return profiles.GetProfile(profileName);
|
||||
}
|
||||
}
|
||||
@@ -223,7 +181,8 @@ namespace PowerDisplay.Common.Services
|
||||
}
|
||||
|
||||
// Internal methods without lock for use within already-locked contexts
|
||||
private static PowerDisplayProfiles LoadProfilesInternal()
|
||||
// Returns tuple with result and optional log message
|
||||
private static (PowerDisplayProfiles Profiles, string? Message) LoadProfilesInternal()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -239,26 +198,31 @@ namespace PowerDisplay.Common.Services
|
||||
if (profiles != null)
|
||||
{
|
||||
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
|
||||
return profiles;
|
||||
return (profiles, $"Loaded {profiles.Profiles.Count} profiles from {filePath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return (new PowerDisplayProfiles(), $"No profiles file found at {filePath}, returning empty collection");
|
||||
}
|
||||
|
||||
return new PowerDisplayProfiles();
|
||||
return (new PowerDisplayProfiles(), null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{LogPrefix} Failed to load profiles internally: {ex.Message}");
|
||||
return new PowerDisplayProfiles();
|
||||
Logger.LogError($"{LogPrefix} Failed to load profiles: {ex.Message}");
|
||||
return (new PowerDisplayProfiles(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool SaveProfilesInternal(PowerDisplayProfiles profiles)
|
||||
// Returns tuple with success status and optional log message
|
||||
private static (bool Success, string? Message) SaveProfilesInternal(PowerDisplayProfiles profiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (profiles == null)
|
||||
{
|
||||
return false;
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
PathConstants.EnsurePowerDisplayFolderExists();
|
||||
@@ -270,12 +234,12 @@ namespace PowerDisplay.Common.Services
|
||||
var filePath = PathConstants.ProfilesFilePath;
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
return true;
|
||||
return (true, $"Saved {profiles.Profiles.Count} profiles to {filePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{LogPrefix} Failed to save profiles internally: {ex.Message}");
|
||||
return false;
|
||||
Logger.LogError($"{LogPrefix} Failed to save profiles: {ex.Message}");
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,54 +18,21 @@ namespace PowerDisplay.Common.Utils
|
||||
/// </summary>
|
||||
/// <param name="monitor">The monitor data to generate a key for.</param>
|
||||
/// <returns>A unique string key for the monitor.</returns>
|
||||
public static string GetMonitorKey(IMonitorData monitor)
|
||||
{
|
||||
if (monitor == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Use hardware ID if available (most stable identifier)
|
||||
if (!string.IsNullOrEmpty(monitor.HardwareId))
|
||||
{
|
||||
return monitor.HardwareId;
|
||||
}
|
||||
|
||||
// Fall back to Id (InternalName in MonitorInfo)
|
||||
if (!string.IsNullOrEmpty(monitor.Id))
|
||||
{
|
||||
return monitor.Id;
|
||||
}
|
||||
|
||||
// Last resort: use display name
|
||||
return monitor.Name ?? string.Empty;
|
||||
}
|
||||
public static string GetMonitorKey(IMonitorData? monitor)
|
||||
=> GetMonitorKey(monitor?.HardwareId, monitor?.Id, monitor?.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a unique key for monitor matching using explicit values.
|
||||
/// Useful when you don't have an IMonitorData object.
|
||||
/// Uses priority: HardwareId > InternalName > Name.
|
||||
/// </summary>
|
||||
/// <param name="hardwareId">The monitor's hardware ID.</param>
|
||||
/// <param name="internalName">The monitor's internal name (optional fallback).</param>
|
||||
/// <param name="name">The monitor's display name (optional fallback).</param>
|
||||
/// <returns>A unique string key for the monitor.</returns>
|
||||
public static string GetMonitorKey(string? hardwareId, string? internalName = null, string? name = null)
|
||||
{
|
||||
// Use hardware ID if available (most stable identifier)
|
||||
if (!string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
return hardwareId;
|
||||
}
|
||||
|
||||
// Fall back to internal name
|
||||
if (!string.IsNullOrEmpty(internalName))
|
||||
{
|
||||
return internalName;
|
||||
}
|
||||
|
||||
// Last resort: use display name
|
||||
return name ?? string.Empty;
|
||||
}
|
||||
=> !string.IsNullOrEmpty(hardwareId) ? hardwareId
|
||||
: !string.IsNullOrEmpty(internalName) ? internalName
|
||||
: name ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Check if two monitors are considered the same based on their keys.
|
||||
|
||||
@@ -28,18 +28,23 @@ namespace PowerDisplay.Common.Utils
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Reverse mapping from Kelvin to VCP value.
|
||||
/// Reverse mapping from Kelvin to VCP value. Auto-generated from VcpToKelvinMap.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, int> KelvinToVcpMap = new()
|
||||
private static readonly Dictionary<int, int> KelvinToVcpMap = BuildReverseMap(VcpToKelvinMap);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a reverse lookup dictionary from the source mapping.
|
||||
/// </summary>
|
||||
private static Dictionary<int, int> BuildReverseMap(Dictionary<int, int> source)
|
||||
{
|
||||
[4000] = 0x03,
|
||||
[5000] = 0x04,
|
||||
[6500] = 0x05,
|
||||
[7500] = 0x06,
|
||||
[9300] = 0x08,
|
||||
[10000] = 0x09,
|
||||
[11500] = 0x0A,
|
||||
};
|
||||
var result = new Dictionary<int, int>();
|
||||
foreach (var kvp in source)
|
||||
{
|
||||
result[kvp.Value] = kvp.Key;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a VCP color temperature preset value to approximate Kelvin temperature.
|
||||
@@ -99,35 +104,33 @@ namespace PowerDisplay.Common.Utils
|
||||
return VcpValueNames.GetName(ColorTemperatureHelper.ColorTemperatureVcpCode, vcpValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a percentage value for display.
|
||||
/// </summary>
|
||||
/// <param name="value">Value (0-100).</param>
|
||||
/// <returns>Formatted string like "50%".</returns>
|
||||
public static string FormatPercentage(int value) => $"{value}%";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a brightness value for display.
|
||||
/// </summary>
|
||||
/// <param name="brightness">Brightness value (0-100).</param>
|
||||
/// <returns>Formatted string like "50%".</returns>
|
||||
public static string FormatBrightness(int brightness)
|
||||
{
|
||||
return $"{brightness}%";
|
||||
}
|
||||
public static string FormatBrightness(int brightness) => FormatPercentage(brightness);
|
||||
|
||||
/// <summary>
|
||||
/// Formats a contrast value for display.
|
||||
/// </summary>
|
||||
/// <param name="contrast">Contrast value (0-100).</param>
|
||||
/// <returns>Formatted string like "50%".</returns>
|
||||
public static string FormatContrast(int contrast)
|
||||
{
|
||||
return $"{contrast}%";
|
||||
}
|
||||
public static string FormatContrast(int contrast) => FormatPercentage(contrast);
|
||||
|
||||
/// <summary>
|
||||
/// Formats a volume value for display.
|
||||
/// </summary>
|
||||
/// <param name="volume">Volume value (0-100).</param>
|
||||
/// <returns>Formatted string like "50%".</returns>
|
||||
public static string FormatVolume(int volume)
|
||||
{
|
||||
return $"{volume}%";
|
||||
}
|
||||
public static string FormatVolume(int volume) => FormatPercentage(volume);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a VCP value represents a known color temperature preset.
|
||||
|
||||
@@ -25,33 +25,33 @@ namespace PowerDisplay.Common.Utils
|
||||
/// <param name="existingProfiles">The collection of existing profiles.</param>
|
||||
/// <param name="baseName">The base name to use (default: "Profile").</param>
|
||||
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
|
||||
public static string GenerateUniqueProfileName(PowerDisplayProfiles existingProfiles, string baseName = DefaultProfileBaseName)
|
||||
public static string GenerateUniqueProfileName(PowerDisplayProfiles? existingProfiles, string baseName = DefaultProfileBaseName)
|
||||
=> GenerateUniqueProfileName(
|
||||
existingProfiles?.Profiles?.Select(p => p.Name).Where(n => !string.IsNullOrEmpty(n))!,
|
||||
baseName);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a unique profile name from a collection of profile names.
|
||||
/// </summary>
|
||||
/// <param name="existingNames">Enumerable of existing profile names.</param>
|
||||
/// <param name="baseName">The base name to use (default: "Profile").</param>
|
||||
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
|
||||
public static string GenerateUniqueProfileName(IEnumerable<string>? existingNames, string baseName = DefaultProfileBaseName)
|
||||
{
|
||||
var existingNames = new HashSet<string>();
|
||||
|
||||
if (existingProfiles?.Profiles != null)
|
||||
{
|
||||
foreach (var profile in existingProfiles.Profiles)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(profile.Name))
|
||||
{
|
||||
existingNames.Add(profile.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GenerateUniqueProfileName(existingNames, baseName);
|
||||
var nameSet = existingNames != null ? new HashSet<string>(existingNames) : null;
|
||||
return GenerateUniqueProfileName(nameSet, baseName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a unique profile name that doesn't conflict with existing names.
|
||||
/// Core implementation used by all overloads.
|
||||
/// </summary>
|
||||
/// <param name="existingNames">Set of existing profile names.</param>
|
||||
/// <param name="baseName">The base name to use (default: "Profile").</param>
|
||||
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
|
||||
public static string GenerateUniqueProfileName(ISet<string> existingNames, string baseName = DefaultProfileBaseName)
|
||||
public static string GenerateUniqueProfileName(ISet<string>? existingNames, string baseName = DefaultProfileBaseName)
|
||||
{
|
||||
if (existingNames == null)
|
||||
if (existingNames == null || existingNames.Count == 0)
|
||||
{
|
||||
return $"{baseName} 1";
|
||||
}
|
||||
@@ -68,18 +68,6 @@ namespace PowerDisplay.Common.Utils
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a unique profile name from a collection of profile names.
|
||||
/// </summary>
|
||||
/// <param name="existingNames">Enumerable of existing profile names.</param>
|
||||
/// <param name="baseName">The base name to use (default: "Profile").</param>
|
||||
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
|
||||
public static string GenerateUniqueProfileName(IEnumerable<string> existingNames, string baseName = DefaultProfileBaseName)
|
||||
{
|
||||
var nameSet = new HashSet<string>(existingNames ?? Enumerable.Empty<string>());
|
||||
return GenerateUniqueProfileName(nameSet, baseName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate that a profile has at least one monitor with at least one setting.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace PowerDisplay.Core
|
||||
public partial class MonitorManager : IDisposable
|
||||
{
|
||||
private readonly List<Monitor> _monitors = new();
|
||||
private readonly Dictionary<string, Monitor> _monitorLookup = new();
|
||||
private readonly List<IMonitorController> _controllers = new();
|
||||
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
||||
private bool _disposed;
|
||||
@@ -180,9 +181,14 @@ namespace PowerDisplay.Core
|
||||
newMonitors.AddRange(validMonitors);
|
||||
}
|
||||
|
||||
// Update monitor list
|
||||
// Update monitor list and lookup dictionary
|
||||
_monitors.Clear();
|
||||
_monitorLookup.Clear();
|
||||
_monitors.AddRange(newMonitors);
|
||||
foreach (var monitor in newMonitors)
|
||||
{
|
||||
_monitorLookup[monitor.Id] = monitor;
|
||||
}
|
||||
|
||||
// Trigger change events
|
||||
var addedMonitors = newMonitors.Where(m => !oldMonitors.Any(o => o.Id == m.Id)).ToList();
|
||||
@@ -394,11 +400,11 @@ namespace PowerDisplay.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor by ID
|
||||
/// Get monitor by ID. Uses dictionary lookup for O(1) performance.
|
||||
/// </summary>
|
||||
public Monitor? GetMonitor(string monitorId)
|
||||
{
|
||||
return _monitors.FirstOrDefault(m => m.Id == monitorId);
|
||||
return _monitorLookup.TryGetValue(monitorId, out var monitor) ? monitor : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -522,6 +528,7 @@ namespace PowerDisplay.Core
|
||||
|
||||
_controllers.Clear();
|
||||
_monitors.Clear();
|
||||
_monitorLookup.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,14 +203,7 @@ namespace PowerDisplay
|
||||
// Allow window to close if program is exiting
|
||||
if (_isExiting)
|
||||
{
|
||||
// Clean up event subscriptions
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
UnsubscribeFromViewModelEvents();
|
||||
args.Handled = false;
|
||||
return;
|
||||
}
|
||||
@@ -220,6 +213,19 @@ namespace PowerDisplay
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from all ViewModel events to prevent memory leaks.
|
||||
/// </summary>
|
||||
private void UnsubscribeFromViewModelEvents()
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowWindow()
|
||||
{
|
||||
try
|
||||
@@ -388,16 +394,8 @@ namespace PowerDisplay
|
||||
_isExiting = true;
|
||||
|
||||
// Quick cleanup of ViewModel
|
||||
if (_viewModel != null)
|
||||
{
|
||||
// Unsubscribe from events
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
|
||||
// Dispose immediately
|
||||
_viewModel.Dispose();
|
||||
}
|
||||
UnsubscribeFromViewModelEvents();
|
||||
_viewModel?.Dispose();
|
||||
|
||||
// Close window directly without animations
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
@@ -20,6 +20,11 @@ namespace PowerDisplay.ViewModels;
|
||||
/// </summary>
|
||||
public partial class MainViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if a value is within the valid range (inclusive).
|
||||
/// </summary>
|
||||
private static bool IsValueInRange(int value, int min, int max) => value >= min && value <= max;
|
||||
|
||||
/// <summary>
|
||||
/// Apply settings changes from Settings UI (IPC event handler entry point)
|
||||
/// Only applies UI configuration changes. Hardware parameter changes (e.g., color temperature)
|
||||
@@ -257,21 +262,21 @@ public partial class MainViewModel
|
||||
|
||||
// Apply brightness if included in profile
|
||||
if (setting.Brightness.HasValue &&
|
||||
setting.Brightness.Value >= monitorVm.MinBrightness && setting.Brightness.Value <= monitorVm.MaxBrightness)
|
||||
IsValueInRange(setting.Brightness.Value, monitorVm.MinBrightness, monitorVm.MaxBrightness))
|
||||
{
|
||||
updateTasks.Add(monitorVm.SetBrightnessAsync(setting.Brightness.Value, immediate: true, fromProfile: true));
|
||||
}
|
||||
|
||||
// Apply contrast if supported and value provided
|
||||
if (setting.Contrast.HasValue && monitorVm.ShowContrast &&
|
||||
setting.Contrast.Value >= monitorVm.MinContrast && setting.Contrast.Value <= monitorVm.MaxContrast)
|
||||
IsValueInRange(setting.Contrast.Value, monitorVm.MinContrast, monitorVm.MaxContrast))
|
||||
{
|
||||
updateTasks.Add(monitorVm.SetContrastAsync(setting.Contrast.Value, immediate: true, fromProfile: true));
|
||||
}
|
||||
|
||||
// Apply volume if supported and value provided
|
||||
if (setting.Volume.HasValue && monitorVm.ShowVolume &&
|
||||
setting.Volume.Value >= monitorVm.MinVolume && setting.Volume.Value <= monitorVm.MaxVolume)
|
||||
IsValueInRange(setting.Volume.Value, monitorVm.MinVolume, monitorVm.MaxVolume))
|
||||
{
|
||||
updateTasks.Add(monitorVm.SetVolumeAsync(setting.Volume.Value, immediate: true, fromProfile: true));
|
||||
}
|
||||
@@ -347,7 +352,7 @@ public partial class MainViewModel
|
||||
{
|
||||
// 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)
|
||||
if (IsValueInRange(savedState.Value.Brightness, monitorVm.MinBrightness, monitorVm.MaxBrightness))
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Brightness), savedState.Value.Brightness);
|
||||
}
|
||||
@@ -366,16 +371,14 @@ public partial class MainViewModel
|
||||
|
||||
// Contrast validation - only apply if hardware supports it
|
||||
if (monitorVm.ShowContrast &&
|
||||
savedState.Value.Contrast >= monitorVm.MinContrast &&
|
||||
savedState.Value.Contrast <= monitorVm.MaxContrast)
|
||||
IsValueInRange(savedState.Value.Contrast, monitorVm.MinContrast, monitorVm.MaxContrast))
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Contrast), savedState.Value.Contrast);
|
||||
}
|
||||
|
||||
// Volume validation - only apply if hardware supports it
|
||||
if (monitorVm.ShowVolume &&
|
||||
savedState.Value.Volume >= monitorVm.MinVolume &&
|
||||
savedState.Value.Volume <= monitorVm.MaxVolume)
|
||||
IsValueInRange(savedState.Value.Volume, monitorVm.MinVolume, monitorVm.MaxVolume))
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Volume), savedState.Value.Volume);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,36 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
// Cached color temperature presets (computed from VcpCodesFormatted)
|
||||
private ObservableCollection<ColorPresetItem> _availableColorPresetsCache;
|
||||
private ObservableCollection<ColorPresetItem> _colorPresetsForDisplayCache;
|
||||
private int _lastColorTemperatureVcpForCache = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the color preset cache and notifies property changes.
|
||||
/// Call this when VcpCodesFormatted or SupportsColorTemperature changes.
|
||||
/// </summary>
|
||||
private void InvalidateColorPresetCache()
|
||||
{
|
||||
_availableColorPresetsCache = null;
|
||||
_colorPresetsForDisplayCache = null;
|
||||
_lastColorTemperatureVcpForCache = -1;
|
||||
OnPropertyChanged(nameof(AvailableColorPresets));
|
||||
OnPropertyChanged(nameof(ColorPresetsForDisplay));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a hexadecimal string (with or without "0x" prefix) to an integer.
|
||||
/// </summary>
|
||||
private static bool TryParseHexCode(string hexString, out int result)
|
||||
{
|
||||
result = 0;
|
||||
if (string.IsNullOrEmpty(hexString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cleanHex = hexString.Replace("0x", string.Empty);
|
||||
return int.TryParse(cleanHex, System.Globalization.NumberStyles.HexNumber, null, out result);
|
||||
}
|
||||
|
||||
public MonitorInfo()
|
||||
{
|
||||
@@ -274,9 +304,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
if (_vcpCodesFormatted != value)
|
||||
{
|
||||
_vcpCodesFormatted = value ?? new List<VcpCodeDisplayInfo>();
|
||||
_availableColorPresetsCache = null; // Clear cache when VCP codes change
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(AvailableColorPresets));
|
||||
InvalidateColorPresetCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,11 +367,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
if (_supportsColorTemperature != value)
|
||||
{
|
||||
_supportsColorTemperature = value;
|
||||
_availableColorPresetsCache = null; // Clear cache when support status changes
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorTemperatureTooltip));
|
||||
OnPropertyChanged(nameof(AvailableColorPresets)); // Refresh computed property
|
||||
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Refresh display list
|
||||
InvalidateColorPresetCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -412,14 +439,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
// Find VCP code 0x14 (Color Temperature / Select Color Preset)
|
||||
var colorTempVcp = _vcpCodesFormatted.FirstOrDefault(v =>
|
||||
{
|
||||
if (int.TryParse(v.Code?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int code))
|
||||
{
|
||||
return code == ColorTemperatureHelper.ColorTemperatureVcpCode;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
TryParseHexCode(v.Code, out int code) && code == ColorTemperatureHelper.ColorTemperatureVcpCode);
|
||||
|
||||
// No VCP 0x14 or no values
|
||||
if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
|
||||
@@ -431,8 +451,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
var colorTempValues = colorTempVcp.ValueList
|
||||
.Select(valueInfo =>
|
||||
{
|
||||
int.TryParse(valueInfo.Value?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int vcpValue);
|
||||
return (VcpValue: vcpValue, Name: valueInfo.Name);
|
||||
bool parsed = TryParseHexCode(valueInfo.Value, out int vcpValue);
|
||||
return (VcpValue: parsed ? vcpValue : 0, Name: valueInfo.Name);
|
||||
})
|
||||
.Where(x => x.VcpValue > 0);
|
||||
|
||||
@@ -443,17 +463,26 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color presets for display in ComboBox, includes current value if not in preset list
|
||||
/// Color presets for display in ComboBox, includes current value if not in preset list.
|
||||
/// Uses caching to avoid recreating collections on every access.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<ColorPresetItem> ColorPresetsForDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
// Return cached value if available and color temperature hasn't changed
|
||||
if (_colorPresetsForDisplayCache != null && _lastColorTemperatureVcpForCache == _colorTemperatureVcp)
|
||||
{
|
||||
return _colorPresetsForDisplayCache;
|
||||
}
|
||||
|
||||
var presets = AvailableColorPresets;
|
||||
if (presets == null || presets.Count == 0)
|
||||
{
|
||||
return new ObservableCollection<ColorPresetItem>();
|
||||
_colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>();
|
||||
_lastColorTemperatureVcpForCache = _colorTemperatureVcp;
|
||||
return _colorPresetsForDisplayCache;
|
||||
}
|
||||
|
||||
// Check if current value is in the preset list
|
||||
@@ -462,20 +491,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
if (currentValueInList)
|
||||
{
|
||||
// Current value is in the list, return as-is
|
||||
return presets;
|
||||
_colorPresetsForDisplayCache = presets;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Current value is not in the preset list - add it at the beginning
|
||||
var displayList = new List<ColorPresetItem>();
|
||||
|
||||
// Add current value with "Custom" indicator using shared helper
|
||||
var displayName = ColorTemperatureHelper.FormatCustomColorTemperatureDisplayName(_colorTemperatureVcp);
|
||||
displayList.Add(new ColorPresetItem(_colorTemperatureVcp, displayName));
|
||||
|
||||
// Add all supported presets
|
||||
displayList.AddRange(presets);
|
||||
|
||||
_colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>(displayList);
|
||||
}
|
||||
|
||||
// Current value is not in the preset list - add it at the beginning
|
||||
var displayList = new List<ColorPresetItem>();
|
||||
|
||||
// Add current value with "Custom" indicator using shared helper
|
||||
var displayName = ColorTemperatureHelper.FormatCustomColorTemperatureDisplayName(_colorTemperatureVcp);
|
||||
displayList.Add(new ColorPresetItem(_colorTemperatureVcp, displayName));
|
||||
|
||||
// Add all supported presets
|
||||
displayList.AddRange(presets);
|
||||
|
||||
return new ObservableCollection<ColorPresetItem>(displayList);
|
||||
_lastColorTemperatureVcpForCache = _colorTemperatureVcp;
|
||||
return _colorPresetsForDisplayCache;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -506,22 +506,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load profiles data from disk
|
||||
/// </summary>
|
||||
private PowerDisplayProfiles LoadProfilesFromDisk()
|
||||
{
|
||||
return ProfileService.LoadProfiles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save profiles data to disk
|
||||
/// </summary>
|
||||
private void SaveProfilesToDisk(PowerDisplayProfiles profiles)
|
||||
{
|
||||
ProfileService.SaveProfiles(profiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a profile to monitors
|
||||
/// </summary>
|
||||
@@ -583,9 +567,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
Logger.LogInfo($"Creating profile: {profile.Name}");
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
var profilesData = ProfileService.LoadProfiles();
|
||||
profilesData.SetProfile(profile);
|
||||
SaveProfilesToDisk(profilesData);
|
||||
ProfileService.SaveProfiles(profilesData);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
@@ -613,12 +597,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
Logger.LogInfo($"Updating profile: {oldName} -> {newProfile.Name}");
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
var profilesData = ProfileService.LoadProfiles();
|
||||
|
||||
// Remove old profile and add updated one
|
||||
profilesData.RemoveProfile(oldName);
|
||||
profilesData.SetProfile(newProfile);
|
||||
SaveProfilesToDisk(profilesData);
|
||||
ProfileService.SaveProfiles(profilesData);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
@@ -645,9 +629,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
Logger.LogInfo($"Deleting profile: {profileName}");
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
var profilesData = ProfileService.LoadProfiles();
|
||||
profilesData.RemoveProfile(profileName);
|
||||
SaveProfilesToDisk(profilesData);
|
||||
ProfileService.SaveProfiles(profilesData);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
|
||||
@@ -43,23 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
item.SuppressAutoSelection = false;
|
||||
|
||||
// Subscribe to selection and checkbox changes
|
||||
item.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MonitorSelectionItem.IsSelected))
|
||||
{
|
||||
OnPropertyChanged(nameof(CanSave));
|
||||
OnPropertyChanged(nameof(HasSelectedMonitors));
|
||||
OnPropertyChanged(nameof(HasValidSettings));
|
||||
}
|
||||
else if (e.PropertyName == nameof(MonitorSelectionItem.IncludeBrightness) ||
|
||||
e.PropertyName == nameof(MonitorSelectionItem.IncludeContrast) ||
|
||||
e.PropertyName == nameof(MonitorSelectionItem.IncludeVolume) ||
|
||||
e.PropertyName == nameof(MonitorSelectionItem.IncludeColorTemperature))
|
||||
{
|
||||
OnPropertyChanged(nameof(CanSave));
|
||||
OnPropertyChanged(nameof(HasValidSettings));
|
||||
}
|
||||
};
|
||||
item.PropertyChanged += OnMonitorItemPropertyChanged;
|
||||
|
||||
_monitors.Add(item);
|
||||
}
|
||||
@@ -122,5 +106,29 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle property changes from monitor selection items.
|
||||
/// Centralizes validation state updates to avoid duplication.
|
||||
/// </summary>
|
||||
private void OnMonitorItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// Update selection-dependent properties
|
||||
if (e.PropertyName == nameof(MonitorSelectionItem.IsSelected))
|
||||
{
|
||||
OnPropertyChanged(nameof(HasSelectedMonitors));
|
||||
}
|
||||
|
||||
// Update validation state for relevant property changes
|
||||
if (e.PropertyName == nameof(MonitorSelectionItem.IsSelected) ||
|
||||
e.PropertyName == nameof(MonitorSelectionItem.IncludeBrightness) ||
|
||||
e.PropertyName == nameof(MonitorSelectionItem.IncludeContrast) ||
|
||||
e.PropertyName == nameof(MonitorSelectionItem.IncludeVolume) ||
|
||||
e.PropertyName == nameof(MonitorSelectionItem.IncludeColorTemperature))
|
||||
{
|
||||
OnPropertyChanged(nameof(CanSave));
|
||||
OnPropertyChanged(nameof(HasValidSettings));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user