Add profile management system to PowerDisplay

Introduced a comprehensive profile management system for PowerDisplay, enabling users to create, edit, delete, and apply predefined monitor settings. Key changes include:

- Added `ProfileManager` for handling profile storage and retrieval.
- Introduced `PowerDisplayProfile`, `PowerDisplayProfiles`, and related data models for profile representation.
- Enhanced `MainViewModel` and `MonitorViewModel` to support profile application and parameter change detection.
- Created `ProfileEditorDialog` for editing and creating profiles via the UI.
- Updated `PowerDisplayViewModel` to manage profiles, including commands for adding, deleting, renaming, and saving profiles.
- Added new events (`ApplyProfileEvent`) and constants for profile application.
- Updated `PowerDisplayPage` UI to include a "Profiles" section for managing profiles.
- Added serialization support for profile-related classes.
- Updated `dllmain.cpp` and `App.xaml.cs` to handle profile-related events.

These changes improve user experience by allowing quick switching between tailored monitor configurations.
This commit is contained in:
Yu Leng
2025-11-19 17:18:01 +08:00
parent fc54172e13
commit b8abff02ac
22 changed files with 1715 additions and 19 deletions

View File

@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class PowerDisplayViewModel : PageViewModelBase
{
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true };
protected override string ModuleName => PowerDisplaySettings.ModuleName;
private GeneralSettings GeneralSettingsConfig { get; set; }
@@ -66,6 +68,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
// Load profiles
LoadProfiles();
// Listen for monitor refresh events from PowerDisplay.exe
NativeEventWaiter.WaitForEventLoop(
Constants.RefreshPowerDisplayMonitorsEvent(),
@@ -469,6 +474,83 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _hasMonitors;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
// Profile-related fields
private ObservableCollection<string> _profiles = new ObservableCollection<string>();
private string _selectedProfile = PowerDisplayProfiles.CustomProfileName;
private string _currentProfile = PowerDisplayProfiles.CustomProfileName;
private string _profilesFilePath = string.Empty;
/// <summary>
/// Collection of available profile names (including Custom)
/// </summary>
public ObservableCollection<string> Profiles
{
get => _profiles;
set
{
if (_profiles != value)
{
_profiles = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Currently selected profile in the ComboBox
/// </summary>
public string SelectedProfile
{
get => _selectedProfile;
set
{
if (_selectedProfile != value && !string.IsNullOrEmpty(value))
{
_selectedProfile = value;
OnPropertyChanged();
// Apply the selected profile
ApplyProfile(value);
}
}
}
/// <summary>
/// Currently active profile (read from settings, may differ from selected during transition)
/// </summary>
public string CurrentProfile
{
get => _currentProfile;
set
{
if (_currentProfile != value)
{
_currentProfile = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsCustomProfile));
}
}
}
/// <summary>
/// True if current profile is Custom
/// </summary>
public bool IsCustomProfile => _currentProfile?.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase) ?? true;
/// <summary>
/// True if a non-Custom profile is selected (enables delete/rename)
/// </summary>
public bool CanModifySelectedProfile => !string.IsNullOrEmpty(_selectedProfile) &&
!_selectedProfile.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase);
public ButtonClickCommand AddProfileCommand => new ButtonClickCommand(AddProfile);
public ButtonClickCommand DeleteProfileCommand => new ButtonClickCommand(DeleteProfile);
public ButtonClickCommand RenameProfileCommand => new ButtonClickCommand(RenameProfile);
public ButtonClickCommand SaveAsProfileCommand => new ButtonClickCommand(SaveAsProfile);
public void RefreshEnabledState()
{
InitializeEnabledValue();
@@ -488,6 +570,323 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return true;
}
/// <summary>
/// Load profiles from disk
/// </summary>
private void LoadProfiles()
{
try
{
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
_profilesFilePath = Path.Combine(powerToysPath, "profiles.json");
var profilesData = LoadProfilesFromDisk();
// Build profile names list
var profileNames = new List<string> { PowerDisplayProfiles.CustomProfileName };
profileNames.AddRange(profilesData.Profiles.Select(p => p.Name));
Profiles = new ObservableCollection<string>(profileNames);
// Set current profile from settings
CurrentProfile = _settings.Properties.CurrentProfile ?? PowerDisplayProfiles.CustomProfileName;
_selectedProfile = CurrentProfile;
OnPropertyChanged(nameof(SelectedProfile));
Logger.LogInfo($"Loaded {profilesData.Profiles.Count} profiles, current: {CurrentProfile}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load profiles: {ex.Message}");
Profiles = new ObservableCollection<string> { PowerDisplayProfiles.CustomProfileName };
CurrentProfile = PowerDisplayProfiles.CustomProfileName;
}
}
/// <summary>
/// Load profiles data from disk
/// </summary>
private PowerDisplayProfiles LoadProfilesFromDisk()
{
if (File.Exists(_profilesFilePath))
{
var json = File.ReadAllText(_profilesFilePath);
var profiles = JsonSerializer.Deserialize<PowerDisplayProfiles>(json);
return profiles ?? new PowerDisplayProfiles();
}
return new PowerDisplayProfiles();
}
/// <summary>
/// Save profiles data to disk
/// </summary>
private void SaveProfilesToDisk(PowerDisplayProfiles profiles)
{
try
{
profiles.LastUpdated = DateTime.UtcNow;
var json = JsonSerializer.Serialize(profiles, _jsonSerializerOptions);
File.WriteAllText(_profilesFilePath, json);
Logger.LogInfo($"Saved profiles to disk: {_profilesFilePath}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to save profiles: {ex.Message}");
}
}
/// <summary>
/// Apply a profile
/// </summary>
private void ApplyProfile(string profileName)
{
try
{
Logger.LogInfo($"Applying profile: {profileName}");
var profilesData = LoadProfilesFromDisk();
var profile = profilesData.GetProfile(profileName);
if (profile == null || !profile.IsValid())
{
Logger.LogWarning($"Profile '{profileName}' not found or invalid");
return;
}
// Create pending operation
var operation = new ProfileOperation(profileName, profile.MonitorSettings);
_settings.Properties.PendingProfileOperation = operation;
_settings.Properties.CurrentProfile = profileName;
// Save settings
NotifySettingsChanged();
// Update current profile
CurrentProfile = profileName;
// Send custom action to trigger profile application
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"action\": {{ \"PowerDisplay\": {{ \"action_name\": \"ApplyProfile\", \"value\": \"{0}\" }} }} }}",
profileName));
// Signal PowerDisplay to apply profile
using (var eventHandle = new System.Threading.EventWaitHandle(
false,
System.Threading.EventResetMode.AutoReset,
Constants.ApplyProfilePowerDisplayEvent()))
{
eventHandle.Set();
}
Logger.LogInfo($"Profile '{profileName}' applied successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to apply profile: {ex.Message}");
}
}
/// <summary>
/// Add a new profile
/// </summary>
private async void AddProfile()
{
try
{
Logger.LogInfo("Adding new profile");
if (Monitors == null || Monitors.Count == 0)
{
Logger.LogWarning("No monitors available to create profile");
return;
}
var profilesData = LoadProfilesFromDisk();
var defaultName = profilesData.GenerateProfileName();
// Show profile editor dialog
var dialog = new Views.ProfileEditorDialog(Monitors, defaultName);
var result = await dialog.ShowAsync();
if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary && dialog.ResultProfile != null)
{
var newProfile = dialog.ResultProfile;
// Validate profile name
if (string.IsNullOrWhiteSpace(newProfile.Name))
{
newProfile = new PowerDisplayProfile(defaultName, newProfile.MonitorSettings);
}
profilesData.SetProfile(newProfile);
SaveProfilesToDisk(profilesData);
// Reload profile list
LoadProfiles();
Logger.LogInfo($"Profile '{newProfile.Name}' created successfully");
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to add profile: {ex.Message}");
}
}
/// <summary>
/// Delete the selected profile
/// </summary>
private void DeleteProfile()
{
try
{
if (!CanModifySelectedProfile)
{
return;
}
Logger.LogInfo($"Deleting profile: {SelectedProfile}");
var profilesData = LoadProfilesFromDisk();
profilesData.RemoveProfile(SelectedProfile);
SaveProfilesToDisk(profilesData);
// Reload profile list
LoadProfiles();
Logger.LogInfo($"Profile '{SelectedProfile}' deleted successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to delete profile: {ex.Message}");
}
}
/// <summary>
/// Rename the selected profile
/// </summary>
private async void RenameProfile()
{
try
{
if (!CanModifySelectedProfile)
{
return;
}
Logger.LogInfo($"Renaming profile: {SelectedProfile}");
// Load the existing profile
var profilesData = LoadProfilesFromDisk();
var existingProfile = profilesData.GetProfile(SelectedProfile);
if (existingProfile == null)
{
Logger.LogWarning($"Profile '{SelectedProfile}' not found");
return;
}
// Show profile editor dialog with existing profile data
var dialog = new Views.ProfileEditorDialog(Monitors, existingProfile.Name);
// Pre-fill monitor settings from existing profile
foreach (var monitorSetting in existingProfile.MonitorSettings)
{
var monitorItem = dialog.ViewModel.Monitors.FirstOrDefault(m => m.Monitor.HardwareId == monitorSetting.HardwareId);
if (monitorItem != null)
{
monitorItem.IsSelected = true;
monitorItem.Brightness = monitorSetting.Brightness;
monitorItem.ColorTemperature = monitorSetting.ColorTemperature;
if (monitorSetting.Contrast.HasValue)
{
monitorItem.Contrast = monitorSetting.Contrast.Value;
}
if (monitorSetting.Volume.HasValue)
{
monitorItem.Volume = monitorSetting.Volume.Value;
}
}
}
var result = await dialog.ShowAsync();
if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary && dialog.ResultProfile != null)
{
var updatedProfile = dialog.ResultProfile;
// Remove old profile and add updated one
profilesData.RemoveProfile(SelectedProfile);
profilesData.SetProfile(updatedProfile);
SaveProfilesToDisk(profilesData);
// Reload profile list
LoadProfiles();
// Select the renamed profile
SelectedProfile = updatedProfile.Name;
Logger.LogInfo($"Profile renamed to '{updatedProfile.Name}' successfully");
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to rename profile: {ex.Message}");
}
}
/// <summary>
/// Save current settings as a new profile
/// </summary>
private void SaveAsProfile()
{
try
{
Logger.LogInfo("Saving current settings as new profile");
var profilesData = LoadProfilesFromDisk();
var newProfileName = profilesData.GenerateProfileName();
// Collect current monitor settings
var monitorSettings = new List<ProfileMonitorSetting>();
foreach (var monitor in Monitors)
{
var setting = new ProfileMonitorSetting(
monitor.HardwareId,
monitor.CurrentBrightness,
monitor.ColorTemperature,
monitor.EnableContrast ? (int?)50 : null,
monitor.EnableVolume ? (int?)50 : null);
monitorSettings.Add(setting);
}
if (monitorSettings.Count == 0)
{
Logger.LogWarning("No monitors available to save profile");
return;
}
var newProfile = new PowerDisplayProfile(newProfileName, monitorSettings);
profilesData.SetProfile(newProfile);
SaveProfilesToDisk(profilesData);
// Reload profile list and select the new profile
LoadProfiles();
SelectedProfile = newProfileName;
Logger.LogInfo($"Saved as profile '{newProfileName}' successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to save as profile: {ex.Message}");
}
}
private void NotifySettingsChanged()
{
// Persist locally first so settings survive even if the module DLL isn't loaded yet.