Refactor PowerDisplay profile management

Simplified profile management by removing the concept of "Custom profiles" and "current profile" tracking. Profiles are now treated as templates for quick application of monitor settings, rather than persistent states.

Key changes include:
- Replaced `ObservableCollection<string>` with `ObservableCollection<PowerDisplayProfile>` to manage profile objects directly.
- Removed redundant properties and methods related to "selected" and "current" profiles.
- Refactored methods for creating, updating, and deleting profiles to operate on `PowerDisplayProfile` objects.
- Updated `PowerDisplayViewModel` and `ProfileManager` to streamline profile loading, saving, and application logic.
- Updated the UI to replace the profile dropdown with buttons for quick application, along with context menu options for managing profiles.
- Improved logging and error handling for profile operations.
- Updated resource strings and removed references to "Custom profiles" and "current profile."

These changes simplify the codebase, improve maintainability, and align the application with the new design philosophy of treating profiles as templates.
This commit is contained in:
Yu Leng
2025-11-20 04:40:36 +08:00
parent b8abff02ac
commit d64bb78727
10 changed files with 5957 additions and 505 deletions

View File

@@ -54,8 +54,11 @@ namespace PowerDisplay.Helpers
if (profiles != null)
{
// Clean up any legacy Custom profiles
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
_cachedProfiles = profiles;
Logger.LogInfo($"Loaded {profiles.Profiles.Count} profiles, current: {profiles.CurrentProfile}");
Logger.LogInfo($"Loaded {profiles.Profiles.Count} profiles");
return profiles;
}
}
@@ -82,12 +85,15 @@ namespace PowerDisplay.Helpers
{
try
{
// 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, AppJsonContext.Default.PowerDisplayProfiles);
File.WriteAllText(_profilesFilePath, json);
_cachedProfiles = profiles;
Logger.LogInfo($"Saved {profiles.Profiles.Count} profiles, current: {profiles.CurrentProfile}");
Logger.LogInfo($"Saved {profiles.Profiles.Count} profiles");
}
catch (Exception ex)
{
@@ -96,62 +102,6 @@ namespace PowerDisplay.Helpers
}
}
/// <summary>
/// Gets the currently active profile
/// </summary>
public PowerDisplayProfile? GetCurrentProfile()
{
var profiles = LoadProfiles();
return profiles.GetCurrentProfile();
}
/// <summary>
/// Sets the current profile by name
/// </summary>
public void SetCurrentProfile(string profileName)
{
lock (_lock)
{
var profiles = LoadProfiles();
// Validate profile exists (unless it's Custom)
if (!profileName.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase))
{
var profile = profiles.GetProfile(profileName);
if (profile == null)
{
Logger.LogWarning($"Cannot set current profile: '{profileName}' not found");
return;
}
}
profiles.CurrentProfile = profileName;
SaveProfiles(profiles);
Logger.LogInfo($"Current profile set to: {profileName}");
}
}
/// <summary>
/// Creates or updates the Custom profile from current monitor states
/// </summary>
public void CreateCustomProfileFromCurrent(List<ProfileMonitorSetting> monitorSettings)
{
lock (_lock)
{
var profiles = LoadProfiles();
var customProfile = new PowerDisplayProfile(
PowerDisplayProfiles.CustomProfileName,
monitorSettings);
profiles.SetProfile(customProfile);
SaveProfiles(profiles);
Logger.LogInfo($"Custom profile created/updated with {monitorSettings.Count} monitors");
}
}
/// <summary>
/// Adds or updates a profile
/// </summary>
@@ -205,23 +155,5 @@ namespace PowerDisplay.Helpers
var profiles = LoadProfiles();
return profiles.Profiles.ToList();
}
/// <summary>
/// Gets the current profile name
/// </summary>
public string GetCurrentProfileName()
{
var profiles = LoadProfiles();
return profiles.CurrentProfile;
}
/// <summary>
/// Checks if currently on a non-Custom profile
/// </summary>
public bool IsOnNonCustomProfile()
{
var currentProfileName = GetCurrentProfileName();
return !currentProfileName.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -17,6 +17,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using PowerDisplay.Commands;
using PowerDisplay.Configuration;
using PowerDisplay.Core;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
@@ -555,9 +556,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
// Apply profile settings to monitors
await ApplyProfileAsync(pendingOp.ProfileName, pendingOp.MonitorSettings);
// Update current profile in profiles.json
_profileManager.SetCurrentProfile(pendingOp.ProfileName);
// Note: We no longer track "current profile" - profiles are just templates
Logger.LogInfo($"[Profile] Successfully applied profile '{pendingOp.ProfileName}'");
}
else
@@ -640,56 +639,14 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
/// <summary>
/// Called when user modifies monitor parameters through PowerDisplay UI
/// Switches to Custom profile if currently on a non-Custom profile
/// Note: Profiles are now templates, not states. Parameter changes are automatically
/// saved to monitor_state.json without any profile state tracking.
/// </summary>
public void OnMonitorParameterChanged(string hardwareId, string propertyName, int value)
{
try
{
// Check if we're currently on a non-Custom profile
if (_profileManager.IsOnNonCustomProfile())
{
var currentProfileName = _profileManager.GetCurrentProfileName();
Logger.LogInfo($"[Profile] Parameter changed while on profile '{currentProfileName}', switching to Custom");
// Create Custom profile from current state
var customSettings = new List<ProfileMonitorSetting>();
foreach (var monitorVm in Monitors)
{
var setting = new ProfileMonitorSetting(
monitorVm.HardwareId,
monitorVm.Brightness,
monitorVm.ColorTemperature,
monitorVm.ShowContrast ? monitorVm.Contrast : null,
monitorVm.ShowVolume ? monitorVm.Volume : null);
customSettings.Add(setting);
}
// Save as Custom profile
_profileManager.CreateCustomProfileFromCurrent(customSettings);
// Set current profile to Custom
_profileManager.SetCurrentProfile(PowerDisplayProfiles.CustomProfileName);
// Update settings.json to reflect Custom profile
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
settings.Properties.CurrentProfile = PowerDisplayProfiles.CustomProfileName;
_settingsUtils.SaveSettings(
System.Text.Json.JsonSerializer.Serialize(settings, AppJsonContext.Default.PowerDisplaySettings),
PowerDisplaySettings.ModuleName);
Logger.LogInfo("[Profile] Switched to Custom profile");
// Notify Settings UI to refresh
NotifySettingsUIRefresh();
}
}
catch (Exception ex)
{
Logger.LogError($"[Profile] Failed to handle parameter change: {ex.Message}");
}
// Parameters are already saved to monitor_state.json via SaveMonitorsToSettings
// No profile state management needed - profiles are just quick-apply templates
Logger.LogDebug($"[Profile] Parameter '{propertyName}' changed for monitor '{hardwareId}' to {value}");
}
/// <summary>
@@ -821,35 +778,8 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
// Read current settings
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
// Check if we should apply a profile on startup
var currentProfileName = _profileManager.GetCurrentProfileName();
if (!string.IsNullOrEmpty(currentProfileName) &&
!currentProfileName.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase))
{
Logger.LogInfo($"[Startup] Applying saved profile: {currentProfileName}");
var currentProfile = _profileManager.GetCurrentProfile();
if (currentProfile != null && currentProfile.IsValid())
{
// Wait for color temperature initialization if needed
if (colorTempInitTasks != null && colorTempInitTasks.Count > 0)
{
await Task.WhenAll(colorTempInitTasks);
}
// Apply profile settings
await ApplyProfileAsync(currentProfileName, currentProfile.MonitorSettings);
StatusText = $"Profile '{currentProfileName}' applied";
IsLoading = false;
return;
}
else
{
Logger.LogWarning($"[Startup] Profile '{currentProfileName}' not found or invalid, falling back to saved state");
}
}
// Note: Profiles are now quick-apply templates, not startup states
// We only restore from monitor_state.json, not from profiles
if (settings.Properties.RestoreSettingsOnStartup)
{
// Restore saved settings from configuration file
@@ -1002,11 +932,11 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
if (monitorVm != null)
{
// Apply default values
monitorVm.Brightness = 30;
monitorVm.Brightness = AppConstants.MonitorDefaults.DefaultBrightness;
// ColorTemperature is now read-only in flyout UI - controlled via Settings UI only
monitorVm.Contrast = 50;
monitorVm.Volume = 50;
monitorVm.Contrast = AppConstants.MonitorDefaults.DefaultContrast;
monitorVm.Volume = AppConstants.MonitorDefaults.DefaultVolume;
StatusText = $"Monitor {monitorVm.Name} reset to default values";
}

View File

@@ -16,21 +16,19 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// </summary>
public class PowerDisplayProfiles
{
// NOTE: Custom profile concept has been removed. Profiles are now templates, not states.
// This constant is kept for backward compatibility (cleaning up legacy Custom profiles).
public const string CustomProfileName = "Custom";
[JsonPropertyName("profiles")]
public List<PowerDisplayProfile> Profiles { get; set; }
[JsonPropertyName("currentProfile")]
public string CurrentProfile { get; set; }
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
public PowerDisplayProfiles()
{
Profiles = new List<PowerDisplayProfile>();
CurrentProfile = CustomProfileName;
LastUpdated = DateTime.UtcNow;
}
@@ -42,14 +40,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Profiles.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Gets the currently active profile
/// </summary>
public PowerDisplayProfile? GetCurrentProfile()
{
return GetProfile(CurrentProfile);
}
/// <summary>
/// Adds or updates a profile
/// </summary>
@@ -76,24 +66,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// </summary>
public bool RemoveProfile(string name)
{
// Cannot remove the Custom profile
if (name.Equals(CustomProfileName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var profile = GetProfile(name);
if (profile != null)
{
Profiles.Remove(profile);
LastUpdated = DateTime.UtcNow;
// If the removed profile was current, switch to Custom
if (CurrentProfile.Equals(name, StringComparison.OrdinalIgnoreCase))
{
CurrentProfile = CustomProfileName;
}
return true;
}
@@ -128,12 +105,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return false;
}
// Custom is reserved
if (name.Equals(CustomProfileName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Check if name is already used (excluding the profile being renamed)
var existing = GetProfile(name);
if (existing != null && (excludeName == null || !existing.Name.Equals(excludeName, StringComparison.OrdinalIgnoreCase)))

View File

@@ -19,7 +19,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
BrightnessUpdateRate = "1s";
Monitors = new List<MonitorInfo>();
RestoreSettingsOnStartup = true;
CurrentProfile = "Custom";
// Note: saved_monitor_settings has been moved to monitor_state.json
// which is managed separately by PowerDisplay app
@@ -37,12 +36,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("restore_settings_on_startup")]
public bool RestoreSettingsOnStartup { get; set; }
/// <summary>
/// Current active profile name (e.g., "Custom", "Profile1", "Profile2")
/// </summary>
[JsonPropertyName("current_profile")]
public string CurrentProfile { get; set; }
/// <summary>
/// Pending color temperature operation from Settings UI.
/// This is cleared after PowerDisplay processes it.

View File

@@ -56,42 +56,67 @@
<controls:SettingsGroup x:Uid="PowerDisplay_Profiles_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_CurrentProfile"
x:Uid="PowerDisplay_QuickProfiles"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}">
<StackPanel Orientation="Horizontal" Spacing="8">
<ComboBox
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedProfile, Mode=TwoWay}"
MinWidth="180" />
<Button
x:Uid="PowerDisplay_AddProfile_Button"
Command="{x:Bind ViewModel.AddProfileCommand}"
ToolTipService.ToolTip="Add new profile">
<FontIcon Glyph="&#xE710;" FontSize="16" />
</Button>
<Button
x:Uid="PowerDisplay_DeleteProfile_Button"
Command="{x:Bind ViewModel.DeleteProfileCommand}"
IsEnabled="{x:Bind ViewModel.CanModifySelectedProfile, Mode=OneWay}"
ToolTipService.ToolTip="Delete selected profile">
<FontIcon Glyph="&#xE74D;" FontSize="16" />
</Button>
<Button
x:Uid="PowerDisplay_SaveAsProfile_Button"
Command="{x:Bind ViewModel.SaveAsProfileCommand}"
ToolTipService.ToolTip="Save current settings as new profile">
<FontIcon Glyph="&#xE74E;" FontSize="16" />
</Button>
</StackPanel>
</tkcontrols:SettingsCard>
<StackPanel Spacing="8">
<!-- Profile buttons row -->
<ItemsRepeater ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Horizontal" Spacing="8"/>
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="library:PowerDisplayProfile">
<Button
Click="ProfileButton_Click"
Tag="{x:Bind}"
ToolTipService.ToolTip="Apply this profile">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE81E;" FontSize="14"/>
<TextBlock Text="{x:Bind Name}"/>
</StackPanel>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_ProfileStatus"
HeaderIcon="{ui:FontIcon Glyph=&#xE946;}">
<TextBlock
Text="{x:Bind ViewModel.CurrentProfile, Mode=OneWay}"
FontWeight="SemiBold"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
<Button.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem
Text="Rename"
Click="RenameProfile_Click"
Tag="{x:Bind}">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE8AC;"/>
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Text="Delete"
Click="DeleteProfile_Click"
Tag="{x:Bind}">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE74D;"/>
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
</Button.ContextFlyout>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<!-- Add profile button -->
<Button
Click="AddProfileButton_Click"
ToolTipService.ToolTip="Save current settings as new profile">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE710;" FontSize="14"/>
<TextBlock x:Uid="PowerDisplay_AddProfile_Text"/>
</StackPanel>
</Button>
<!-- Help text -->
<TextBlock
x:Uid="PowerDisplay_ProfilesHelpText"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"/>
</StackPanel>
</tkcontrols:SettingsCard>
</controls:SettingsGroup>

View File

@@ -153,5 +153,97 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
}
// Profile button event handlers
private void ProfileButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is PowerDisplayProfile profile)
{
ViewModel.ApplyProfile(profile);
}
}
private async void AddProfileButton_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.Monitors == null || ViewModel.Monitors.Count == 0)
{
return;
}
var defaultName = GenerateDefaultProfileName();
var dialog = new ProfileEditorDialog(ViewModel.Monitors, defaultName);
dialog.XamlRoot = this.XamlRoot;
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary && dialog.ResultProfile != null)
{
ViewModel.CreateProfile(dialog.ResultProfile);
}
}
private async void RenameProfile_Click(object sender, RoutedEventArgs e)
{
var menuItem = sender as MenuFlyoutItem;
if (menuItem?.Tag is PowerDisplayProfile profile)
{
var dialog = new ProfileEditorDialog(ViewModel.Monitors, profile.Name);
dialog.XamlRoot = this.XamlRoot;
// Pre-fill with existing profile settings
dialog.PreFillProfile(profile);
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary && dialog.ResultProfile != null)
{
ViewModel.UpdateProfile(profile.Name, dialog.ResultProfile);
}
}
}
private async void DeleteProfile_Click(object sender, RoutedEventArgs e)
{
var menuItem = sender as MenuFlyoutItem;
if (menuItem?.Tag is PowerDisplayProfile profile)
{
var dialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = "Delete Profile",
Content = $"Are you sure you want to delete '{profile.Name}'?",
PrimaryButtonText = "Delete",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Close,
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
ViewModel.DeleteProfile(profile.Name);
}
}
}
private string GenerateDefaultProfileName()
{
var existingNames = new HashSet<string>();
foreach (var profile in ViewModel.Profiles)
{
existingNames.Add(profile.Name);
}
int counter = 1;
string name;
do
{
name = $"Profile {counter}";
counter++;
}
while (existingNames.Contains(name));
return name;
}
}
}

View File

@@ -5,6 +5,7 @@
#nullable enable
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
@@ -39,5 +40,41 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
ResultProfile = null;
}
/// <summary>
/// Pre-fill the dialog with existing profile data
/// </summary>
public void PreFillProfile(PowerDisplayProfile profile)
{
if (profile == null || ViewModel == null)
{
return;
}
// Set profile name
ViewModel.ProfileName = profile.Name;
// Pre-fill monitor settings from existing profile
foreach (var monitorSetting in profile.MonitorSettings)
{
var monitorItem = 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;
}
}
}
}
}
}

View File

@@ -5550,11 +5550,17 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="PowerDisplay_Profiles_GroupSettings.Header" xml:space="preserve">
<value>Profiles</value>
</data>
<data name="PowerDisplay_CurrentProfile.Header" xml:space="preserve">
<value>Profile</value>
<data name="PowerDisplay_QuickProfiles.Header" xml:space="preserve">
<value>Quick apply profiles</value>
</data>
<data name="PowerDisplay_CurrentProfile.Description" xml:space="preserve">
<value>Select a profile to apply predefined monitor settings</value>
<data name="PowerDisplay_QuickProfiles.Description" xml:space="preserve">
<value>Click a profile button to quickly apply saved monitor settings</value>
</data>
<data name="PowerDisplay_AddProfile_Text.Text" xml:space="preserve">
<value>Add Profile</value>
</data>
<data name="PowerDisplay_ProfilesHelpText.Text" xml:space="preserve">
<value>Right-click a profile to rename or delete it</value>
</data>
<data name="PowerDisplay_AddProfile_Button.ToolTipService.ToolTip" xml:space="preserve">
<value>Add new profile</value>
@@ -5562,15 +5568,6 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="PowerDisplay_DeleteProfile_Button.ToolTipService.ToolTip" xml:space="preserve">
<value>Delete selected profile</value>
</data>
<data name="PowerDisplay_SaveAsProfile_Button.ToolTipService.ToolTip" xml:space="preserve">
<value>Save current settings as new profile</value>
</data>
<data name="PowerDisplay_ProfileStatus.Header" xml:space="preserve">
<value>Active profile</value>
</data>
<data name="PowerDisplay_ProfileStatus.Description" xml:space="preserve">
<value>Currently active configuration profile</value>
</data>
<data name="PowerDisplay_Monitor_HideMonitor.Header" xml:space="preserve">
<value>Hide monitor</value>
</data>

File diff suppressed because it is too large Load Diff

View File

@@ -475,15 +475,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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 ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
private string _profilesFilePath = string.Empty;
/// <summary>
/// Collection of available profile names (including Custom)
/// Collection of available profiles (for button display)
/// </summary>
public ObservableCollection<string> Profiles
public ObservableCollection<PowerDisplayProfile> Profiles
{
get => _profiles;
set
@@ -496,61 +494,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
/// <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();
@@ -583,24 +526,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var profilesData = LoadProfilesFromDisk();
// Build profile names list
var profileNames = new List<string> { PowerDisplayProfiles.CustomProfileName };
profileNames.AddRange(profilesData.Profiles.Select(p => p.Name));
// Load profile objects (no Custom - it's not a profile anymore)
Profiles.Clear();
foreach (var profile in profilesData.Profiles)
{
Profiles.Add(profile);
}
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}");
Logger.LogInfo($"Loaded {Profiles.Count} profiles");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load profiles: {ex.Message}");
Profiles = new ObservableCollection<string> { PowerDisplayProfiles.CustomProfileName };
CurrentProfile = PowerDisplayProfiles.CustomProfileName;
Profiles.Clear();
}
}
@@ -638,40 +576,33 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
/// <summary>
/// Apply a profile
/// Apply a profile to monitors
/// </summary>
private void ApplyProfile(string profileName)
public void ApplyProfile(PowerDisplayProfile profile)
{
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");
Logger.LogWarning("Invalid profile");
return;
}
Logger.LogInfo($"Applying profile: {profile.Name}");
// Create pending operation
var operation = new ProfileOperation(profileName, profile.MonitorSettings);
var operation = new ProfileOperation(profile.Name, 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));
profile.Name));
// Signal PowerDisplay to apply profile
using (var eventHandle = new System.Threading.EventWaitHandle(
@@ -682,7 +613,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
eventHandle.Set();
}
Logger.LogInfo($"Profile '{profileName}' applied successfully");
Logger.LogInfo($"Profile '{profile.Name}' applied successfully");
}
catch (Exception ex)
{
@@ -691,74 +622,90 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
/// <summary>
/// Add a new profile
/// Create a new profile
/// </summary>
private async void AddProfile()
public void CreateProfile(PowerDisplayProfile profile)
{
try
{
Logger.LogInfo("Adding new profile");
if (Monitors == null || Monitors.Count == 0)
if (profile == null || !profile.IsValid())
{
Logger.LogWarning("No monitors available to create profile");
Logger.LogWarning("Invalid 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}");
Logger.LogInfo($"Creating profile: {profile.Name}");
var profilesData = LoadProfilesFromDisk();
profilesData.RemoveProfile(SelectedProfile);
profilesData.SetProfile(profile);
SaveProfilesToDisk(profilesData);
// Reload profile list
LoadProfiles();
Logger.LogInfo($"Profile '{SelectedProfile}' deleted successfully");
Logger.LogInfo($"Profile '{profile.Name}' created successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to create profile: {ex.Message}");
}
}
/// <summary>
/// Update an existing profile
/// </summary>
public void UpdateProfile(string oldName, PowerDisplayProfile newProfile)
{
try
{
if (newProfile == null || !newProfile.IsValid())
{
Logger.LogWarning("Invalid profile");
return;
}
Logger.LogInfo($"Updating profile: {oldName} -> {newProfile.Name}");
var profilesData = LoadProfilesFromDisk();
// Remove old profile and add updated one
profilesData.RemoveProfile(oldName);
profilesData.SetProfile(newProfile);
SaveProfilesToDisk(profilesData);
// Reload profile list
LoadProfiles();
Logger.LogInfo($"Profile updated to '{newProfile.Name}' successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to update profile: {ex.Message}");
}
}
/// <summary>
/// Delete a profile
/// </summary>
public void DeleteProfile(string profileName)
{
try
{
if (string.IsNullOrEmpty(profileName))
{
return;
}
Logger.LogInfo($"Deleting profile: {profileName}");
var profilesData = LoadProfilesFromDisk();
profilesData.RemoveProfile(profileName);
SaveProfilesToDisk(profilesData);
// Reload profile list
LoadProfiles();
Logger.LogInfo($"Profile '{profileName}' deleted successfully");
}
catch (Exception ex)
{
@@ -766,127 +713,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
/// <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.