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:
Yu Leng
2025-11-24 23:36:25 +08:00
parent 15746e8f45
commit 471cad659f
12 changed files with 225 additions and 288 deletions

View File

@@ -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>

View File

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

View File

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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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