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

@@ -211,4 +211,12 @@ namespace winrt::PowerToys::Interop::implementation
{ {
return CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT; return CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT;
} }
hstring Constants::ApplyColorTemperaturePowerDisplayEvent()
{
return CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT;
}
hstring Constants::ApplyProfilePowerDisplayEvent()
{
return CommonSharedConstants::APPLY_PROFILE_POWER_DISPLAY_EVENT;
}
} }

View File

@@ -56,6 +56,8 @@ namespace winrt::PowerToys::Interop::implementation
static hstring TerminatePowerDisplayEvent(); static hstring TerminatePowerDisplayEvent();
static hstring RefreshPowerDisplayMonitorsEvent(); static hstring RefreshPowerDisplayMonitorsEvent();
static hstring SettingsUpdatedPowerDisplayEvent(); static hstring SettingsUpdatedPowerDisplayEvent();
static hstring ApplyColorTemperaturePowerDisplayEvent();
static hstring ApplyProfilePowerDisplayEvent();
}; };
} }

View File

@@ -53,6 +53,8 @@ namespace PowerToys
static String TerminatePowerDisplayEvent(); static String TerminatePowerDisplayEvent();
static String RefreshPowerDisplayMonitorsEvent(); static String RefreshPowerDisplayMonitorsEvent();
static String SettingsUpdatedPowerDisplayEvent(); static String SettingsUpdatedPowerDisplayEvent();
static String ApplyColorTemperaturePowerDisplayEvent();
static String ApplyProfilePowerDisplayEvent();
} }
} }
} }

View File

@@ -138,6 +138,7 @@ namespace CommonSharedConstants
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c"; const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e"; const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
const wchar_t APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d"; const wchar_t APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
const wchar_t APPLY_PROFILE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
// used from quick access window // used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a"; const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";

View File

@@ -0,0 +1,227 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Configuration;
using PowerDisplay.Serialization;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Manages PowerDisplay profiles storage and retrieval
/// </summary>
public class ProfileManager
{
private readonly string _profilesFilePath;
private readonly object _lock = new object();
private PowerDisplayProfiles? _cachedProfiles;
public ProfileManager()
{
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
if (!Directory.Exists(powerToysPath))
{
Directory.CreateDirectory(powerToysPath);
}
_profilesFilePath = Path.Combine(powerToysPath, "profiles.json");
Logger.LogInfo($"ProfileManager initialized, profiles file: {_profilesFilePath}");
}
/// <summary>
/// Loads profiles from disk
/// </summary>
public PowerDisplayProfiles LoadProfiles()
{
lock (_lock)
{
try
{
if (File.Exists(_profilesFilePath))
{
var json = File.ReadAllText(_profilesFilePath);
var profiles = JsonSerializer.Deserialize(json, AppJsonContext.Default.PowerDisplayProfiles);
if (profiles != null)
{
_cachedProfiles = profiles;
Logger.LogInfo($"Loaded {profiles.Profiles.Count} profiles, current: {profiles.CurrentProfile}");
return profiles;
}
}
Logger.LogInfo("No profiles file found, creating default");
_cachedProfiles = new PowerDisplayProfiles();
return _cachedProfiles;
}
catch (Exception ex)
{
Logger.LogError($"Failed to load profiles: {ex.Message}");
_cachedProfiles = new PowerDisplayProfiles();
return _cachedProfiles;
}
}
}
/// <summary>
/// Saves profiles to disk
/// </summary>
public void SaveProfiles(PowerDisplayProfiles profiles)
{
lock (_lock)
{
try
{
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}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to save profiles: {ex.Message}");
}
}
}
/// <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>
public void AddOrUpdateProfile(PowerDisplayProfile profile)
{
lock (_lock)
{
if (profile == null || !profile.IsValid())
{
Logger.LogWarning("Cannot add invalid profile");
return;
}
var profiles = LoadProfiles();
profiles.SetProfile(profile);
SaveProfiles(profiles);
Logger.LogInfo($"Profile '{profile.Name}' added/updated with {profile.MonitorSettings.Count} monitors");
}
}
/// <summary>
/// Removes a profile by name
/// </summary>
public bool RemoveProfile(string profileName)
{
lock (_lock)
{
var profiles = LoadProfiles();
bool removed = profiles.RemoveProfile(profileName);
if (removed)
{
SaveProfiles(profiles);
Logger.LogInfo($"Profile '{profileName}' removed");
}
else
{
Logger.LogWarning($"Profile '{profileName}' not found or cannot be removed");
}
return removed;
}
}
/// <summary>
/// Gets all profiles
/// </summary>
public List<PowerDisplayProfile> GetAllProfiles()
{
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

@@ -32,6 +32,7 @@ namespace PowerDisplay
private const string RefreshMonitorsEvent = "Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c"; private const string RefreshMonitorsEvent = "Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
private const string SettingsUpdatedEvent = "Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e"; private const string SettingsUpdatedEvent = "Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
private const string ApplyColorTemperatureEvent = "Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d"; private const string ApplyColorTemperatureEvent = "Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
private const string ApplyProfileEvent = "Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
private Window? _mainWindow; private Window? _mainWindow;
private int _powerToysRunnerPid; private int _powerToysRunnerPid;
@@ -176,6 +177,21 @@ namespace PowerDisplay
}, },
CancellationToken.None); CancellationToken.None);
NativeEventWaiter.WaitForEventLoop(
ApplyProfileEvent,
() =>
{
Logger.LogInfo("Received apply profile event");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
mainWindow.ViewModel.ApplyProfileFromSettings();
}
});
},
CancellationToken.None);
// Monitor Runner process (backup exit mechanism) // Monitor Runner process (backup exit mechanism)
if (_powerToysRunnerPid > 0) if (_powerToysRunnerPid > 0)
{ {

View File

@@ -22,6 +22,10 @@ namespace PowerDisplay.Serialization
[JsonSerializable(typeof(MonitorStateEntry))] [JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(PowerDisplaySettings))] [JsonSerializable(typeof(PowerDisplaySettings))]
[JsonSerializable(typeof(ColorTemperatureOperation))] [JsonSerializable(typeof(ColorTemperatureOperation))]
[JsonSerializable(typeof(ProfileOperation))]
[JsonSerializable(typeof(PowerDisplayProfiles))]
[JsonSerializable(typeof(PowerDisplayProfile))]
[JsonSerializable(typeof(ProfileMonitorSetting))]
// MonitorInfo and related types (Settings.UI.Library) // MonitorInfo and related types (Settings.UI.Library)
[JsonSerializable(typeof(MonitorInfo))] [JsonSerializable(typeof(MonitorInfo))]
@@ -33,6 +37,8 @@ namespace PowerDisplay.Serialization
[JsonSerializable(typeof(List<MonitorInfo>))] [JsonSerializable(typeof(List<MonitorInfo>))]
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))] [JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
[JsonSerializable(typeof(List<VcpValueInfo>))] [JsonSerializable(typeof(List<VcpValueInfo>))]
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
[JsonSerializable(typeof(List<ProfileMonitorSetting>))]
[JsonSourceGenerationOptions( [JsonSourceGenerationOptions(
WriteIndented = true, WriteIndented = true,

View File

@@ -38,6 +38,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
private readonly CancellationTokenSource _cancellationTokenSource; private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ISettingsUtils _settingsUtils; private readonly ISettingsUtils _settingsUtils;
private readonly MonitorStateManager _stateManager; private readonly MonitorStateManager _stateManager;
private readonly ProfileManager _profileManager;
private ObservableCollection<MonitorViewModel> _monitors; private ObservableCollection<MonitorViewModel> _monitors;
private string _statusText; private string _statusText;
@@ -61,6 +62,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
// Initialize settings utils // Initialize settings utils
_settingsUtils = new SettingsUtils(); _settingsUtils = new SettingsUtils();
_stateManager = new MonitorStateManager(); _stateManager = new MonitorStateManager();
_profileManager = new ProfileManager();
// Initialize the monitor manager // Initialize the monitor manager
_monitorManager = new MonitorManager(); _monitorManager = new MonitorManager();
@@ -530,6 +532,188 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
} }
} }
/// <summary>
/// Apply profile from Settings UI (triggered by custom action from Settings UI)
/// This is called when user explicitly switches profile in Settings UI.
/// Reads the pending operation from settings and applies it directly.
/// </summary>
public async void ApplyProfileFromSettings()
{
try
{
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
// Check if there's a pending profile operation
var pendingOp = settings.Properties.PendingProfileOperation;
if (pendingOp != null && !string.IsNullOrEmpty(pendingOp.ProfileName))
{
Logger.LogInfo($"[Profile] Processing pending profile operation: '{pendingOp.ProfileName}' with {pendingOp.MonitorSettings?.Count ?? 0} monitors");
if (pendingOp.MonitorSettings != null && pendingOp.MonitorSettings.Count > 0)
{
// Apply profile settings to monitors
await ApplyProfileAsync(pendingOp.ProfileName, pendingOp.MonitorSettings);
// Update current profile in profiles.json
_profileManager.SetCurrentProfile(pendingOp.ProfileName);
Logger.LogInfo($"[Profile] Successfully applied profile '{pendingOp.ProfileName}'");
}
else
{
Logger.LogWarning($"[Profile] Profile '{pendingOp.ProfileName}' has no monitor settings");
}
// Clear the pending operation
settings.Properties.PendingProfileOperation = null;
_settingsUtils.SaveSettings(
System.Text.Json.JsonSerializer.Serialize(settings, AppJsonContext.Default.PowerDisplaySettings),
PowerDisplaySettings.ModuleName);
Logger.LogInfo("[Profile] Cleared pending profile operation");
}
else
{
Logger.LogInfo("[Profile] No pending profile operation");
}
}
catch (Exception ex)
{
Logger.LogError($"[Profile] Failed to apply profile from settings: {ex.Message}");
}
}
/// <summary>
/// Apply profile settings to monitors
/// </summary>
private async Task ApplyProfileAsync(string profileName, List<ProfileMonitorSetting> monitorSettings)
{
var updateTasks = new List<Task>();
foreach (var setting in monitorSettings)
{
// Find monitor by HardwareId
var monitorVm = Monitors.FirstOrDefault(m => m.HardwareId == setting.HardwareId);
if (monitorVm == null)
{
Logger.LogWarning($"[Profile] Monitor with HardwareId '{setting.HardwareId}' not found (disconnected?)");
continue;
}
Logger.LogInfo($"[Profile] Applying settings to monitor '{monitorVm.Name}' (HardwareId: {setting.HardwareId})");
// Apply brightness
if (setting.Brightness >= monitorVm.MinBrightness && setting.Brightness <= monitorVm.MaxBrightness)
{
updateTasks.Add(monitorVm.SetBrightnessAsync(setting.Brightness, 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)
{
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)
{
updateTasks.Add(monitorVm.SetVolumeAsync(setting.Volume.Value, immediate: true, fromProfile: true));
}
// Apply color temperature
if (setting.ColorTemperature > 0)
{
updateTasks.Add(monitorVm.SetColorTemperatureAsync(setting.ColorTemperature, fromProfile: true));
}
}
// Wait for all updates to complete
if (updateTasks.Count > 0)
{
await Task.WhenAll(updateTasks);
Logger.LogInfo($"[Profile] Applied {updateTasks.Count} parameter updates");
}
}
/// <summary>
/// Called when user modifies monitor parameters through PowerDisplay UI
/// Switches to Custom profile if currently on a non-Custom profile
/// </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}");
}
}
/// <summary>
/// Notifies Settings UI to refresh (e.g., when profile changes)
/// </summary>
private void NotifySettingsUIRefresh()
{
try
{
using (var eventHandle = new System.Threading.EventWaitHandle(
false,
System.Threading.EventResetMode.AutoReset,
Constants.RefreshPowerDisplayMonitorsEvent()))
{
eventHandle.Set();
Logger.LogInfo("[Profile] Notified Settings UI to refresh");
}
}
catch (Exception ex)
{
Logger.LogWarning($"[Profile] Failed to notify Settings UI: {ex.Message}");
}
}
/// <summary> /// <summary>
/// Apply hardware parameter changes (brightness, color temperature) /// Apply hardware parameter changes (brightness, color temperature)
/// Asynchronous operation that communicates with monitor hardware via DDC/CI /// Asynchronous operation that communicates with monitor hardware via DDC/CI
@@ -637,6 +821,35 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
// Read current settings // Read current settings
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay"); 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");
}
}
if (settings.Properties.RestoreSettingsOnStartup) if (settings.Properties.RestoreSettingsOnStartup)
{ {
// Restore saved settings from configuration file // Restore saved settings from configuration file

View File

@@ -77,7 +77,8 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// </summary> /// </summary>
/// <param name="brightness">Brightness value (0-100)</param> /// <param name="brightness">Brightness value (0-100)</param>
/// <param name="immediate">If true, applies immediately; if false, debounces for smooth slider</param> /// <param name="immediate">If true, applies immediately; if false, debounces for smooth slider</param>
public async Task SetBrightnessAsync(int brightness, bool immediate = false) /// <param name="fromProfile">If true, skip profile change detection (avoid recursion)</param>
public async Task SetBrightnessAsync(int brightness, bool immediate = false, bool fromProfile = false)
{ {
brightness = Math.Clamp(brightness, MinBrightness, MaxBrightness); brightness = Math.Clamp(brightness, MinBrightness, MaxBrightness);
@@ -91,20 +92,20 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
// Apply to hardware (with or without debounce) // Apply to hardware (with or without debounce)
if (immediate) if (immediate)
{ {
await ApplyBrightnessToHardwareAsync(brightness); await ApplyBrightnessToHardwareAsync(brightness, fromProfile);
} }
else else
{ {
// Debounce for slider smoothness // Debounce for slider smoothness (always from user interaction, not from profile)
var capturedValue = brightness; var capturedValue = brightness;
_brightnessDebouncer.Debounce(async () => await ApplyBrightnessToHardwareAsync(capturedValue)); _brightnessDebouncer.Debounce(async () => await ApplyBrightnessToHardwareAsync(capturedValue, fromUserInteraction: true));
} }
} }
/// <summary> /// <summary>
/// Unified method to apply contrast with hardware update and state persistence. /// Unified method to apply contrast with hardware update and state persistence.
/// </summary> /// </summary>
public async Task SetContrastAsync(int contrast, bool immediate = false) public async Task SetContrastAsync(int contrast, bool immediate = false, bool fromProfile = false)
{ {
contrast = Math.Clamp(contrast, MinContrast, MaxContrast); contrast = Math.Clamp(contrast, MinContrast, MaxContrast);
@@ -117,19 +118,19 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
if (immediate) if (immediate)
{ {
await ApplyContrastToHardwareAsync(contrast); await ApplyContrastToHardwareAsync(contrast, fromProfile);
} }
else else
{ {
var capturedValue = contrast; var capturedValue = contrast;
_contrastDebouncer.Debounce(async () => await ApplyContrastToHardwareAsync(capturedValue)); _contrastDebouncer.Debounce(async () => await ApplyContrastToHardwareAsync(capturedValue, fromUserInteraction: true));
} }
} }
/// <summary> /// <summary>
/// Unified method to apply volume with hardware update and state persistence. /// Unified method to apply volume with hardware update and state persistence.
/// </summary> /// </summary>
public async Task SetVolumeAsync(int volume, bool immediate = false) public async Task SetVolumeAsync(int volume, bool immediate = false, bool fromProfile = false)
{ {
volume = Math.Clamp(volume, MinVolume, MaxVolume); volume = Math.Clamp(volume, MinVolume, MaxVolume);
@@ -141,12 +142,12 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
if (immediate) if (immediate)
{ {
await ApplyVolumeToHardwareAsync(volume); await ApplyVolumeToHardwareAsync(volume, fromProfile);
} }
else else
{ {
var capturedValue = volume; var capturedValue = volume;
_volumeDebouncer.Debounce(async () => await ApplyVolumeToHardwareAsync(capturedValue)); _volumeDebouncer.Debounce(async () => await ApplyVolumeToHardwareAsync(capturedValue, fromUserInteraction: true));
} }
} }
@@ -154,7 +155,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// Unified method to apply color temperature with hardware update and state persistence. /// Unified method to apply color temperature with hardware update and state persistence.
/// Always immediate (no debouncing for discrete preset values). /// Always immediate (no debouncing for discrete preset values).
/// </summary> /// </summary>
public async Task SetColorTemperatureAsync(int colorTemperature) public async Task SetColorTemperatureAsync(int colorTemperature, bool fromProfile = false)
{ {
try try
{ {
@@ -168,7 +169,14 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(ColorTemperature)); OnPropertyChanged(nameof(ColorTemperature));
OnPropertyChanged(nameof(ColorTemperaturePresetName)); OnPropertyChanged(nameof(ColorTemperaturePresetName));
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "ColorTemperature", colorTemperature); _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(ColorTemperature), colorTemperature);
// Trigger profile change detection if from user interaction
if (!fromProfile)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(ColorTemperature), colorTemperature);
}
Logger.LogInfo($"[{HardwareId}] Color temperature applied successfully"); Logger.LogInfo($"[{HardwareId}] Color temperature applied successfully");
} }
else else
@@ -186,7 +194,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// Internal method - applies brightness to hardware and persists state. /// Internal method - applies brightness to hardware and persists state.
/// Unified logic for all sources (Flyout, Settings, etc.). /// Unified logic for all sources (Flyout, Settings, etc.).
/// </summary> /// </summary>
private async Task ApplyBrightnessToHardwareAsync(int brightness) private async Task ApplyBrightnessToHardwareAsync(int brightness, bool fromUserInteraction = false)
{ {
try try
{ {
@@ -196,7 +204,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
if (result.IsSuccess) if (result.IsSuccess)
{ {
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Brightness", brightness); _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Brightness), brightness);
// Trigger profile change detection if from user interaction
if (fromUserInteraction)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Brightness), brightness);
}
} }
else else
{ {
@@ -212,7 +226,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// <summary> /// <summary>
/// Internal method - applies contrast to hardware and persists state. /// Internal method - applies contrast to hardware and persists state.
/// </summary> /// </summary>
private async Task ApplyContrastToHardwareAsync(int contrast) private async Task ApplyContrastToHardwareAsync(int contrast, bool fromUserInteraction = false)
{ {
try try
{ {
@@ -222,7 +236,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
if (result.IsSuccess) if (result.IsSuccess)
{ {
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Contrast", contrast); _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Contrast), contrast);
// Trigger profile change detection if from user interaction
if (fromUserInteraction)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Contrast), contrast);
}
} }
else else
{ {
@@ -238,7 +258,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// <summary> /// <summary>
/// Internal method - applies volume to hardware and persists state. /// Internal method - applies volume to hardware and persists state.
/// </summary> /// </summary>
private async Task ApplyVolumeToHardwareAsync(int volume) private async Task ApplyVolumeToHardwareAsync(int volume, bool fromUserInteraction = false)
{ {
try try
{ {
@@ -248,7 +268,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
if (result.IsSuccess) if (result.IsSuccess)
{ {
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Volume", volume); _mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Volume), volume);
// Trigger profile change detection if from user interaction
if (fromUserInteraction)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Volume), volume);
}
} }
else else
{ {

View File

@@ -62,6 +62,7 @@ private:
HANDLE m_hRefreshEvent = nullptr; HANDLE m_hRefreshEvent = nullptr;
HANDLE m_hSettingsUpdatedEvent = nullptr; HANDLE m_hSettingsUpdatedEvent = nullptr;
HANDLE m_hApplyColorTemperatureEvent = nullptr; HANDLE m_hApplyColorTemperatureEvent = nullptr;
HANDLE m_hApplyProfileEvent = nullptr;
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings) void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
{ {
@@ -199,8 +200,9 @@ public:
m_hRefreshEvent = CreateDefaultEvent(CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT); m_hRefreshEvent = CreateDefaultEvent(CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT);
m_hSettingsUpdatedEvent = CreateDefaultEvent(CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT); m_hSettingsUpdatedEvent = CreateDefaultEvent(CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT);
m_hApplyColorTemperatureEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT); m_hApplyColorTemperatureEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT);
m_hApplyProfileEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_PROFILE_POWER_DISPLAY_EVENT);
if (!m_hInvokeEvent || !m_hToggleEvent || !m_hTerminateEvent || !m_hRefreshEvent || !m_hSettingsUpdatedEvent || !m_hApplyColorTemperatureEvent) if (!m_hInvokeEvent || !m_hToggleEvent || !m_hTerminateEvent || !m_hRefreshEvent || !m_hSettingsUpdatedEvent || !m_hApplyColorTemperatureEvent || !m_hApplyProfileEvent)
{ {
Logger::error(L"Failed to create one or more event handles"); Logger::error(L"Failed to create one or more event handles");
} }
@@ -244,6 +246,11 @@ public:
CloseHandle(m_hApplyColorTemperatureEvent); CloseHandle(m_hApplyColorTemperatureEvent);
m_hApplyColorTemperatureEvent = nullptr; m_hApplyColorTemperatureEvent = nullptr;
} }
if (m_hApplyProfileEvent)
{
CloseHandle(m_hApplyProfileEvent);
m_hApplyProfileEvent = nullptr;
}
} }
virtual void destroy() override virtual void destroy() override
@@ -327,6 +334,19 @@ public:
Logger::warn(L"Apply color temperature event handle is null"); Logger::warn(L"Apply color temperature event handle is null");
} }
} }
else if (action_object.get_name() == L"ApplyProfile")
{
Logger::trace(L"ApplyProfile action received");
if (m_hApplyProfileEvent)
{
Logger::trace(L"Signaling apply profile event");
SetEvent(m_hApplyProfileEvent);
}
else
{
Logger::warn(L"Apply profile event handle is null");
}
}
} }
catch (std::exception&) catch (std::exception&)
{ {

View File

@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Represents a PowerDisplay profile containing monitor settings
/// </summary>
public class PowerDisplayProfile
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("monitorSettings")]
public List<ProfileMonitorSetting> MonitorSettings { get; set; }
[JsonPropertyName("createdDate")]
public DateTime CreatedDate { get; set; }
[JsonPropertyName("lastModified")]
public DateTime LastModified { get; set; }
public PowerDisplayProfile()
{
Name = string.Empty;
MonitorSettings = new List<ProfileMonitorSetting>();
CreatedDate = DateTime.UtcNow;
LastModified = DateTime.UtcNow;
}
public PowerDisplayProfile(string name, List<ProfileMonitorSetting> monitorSettings)
{
Name = name;
MonitorSettings = monitorSettings ?? new List<ProfileMonitorSetting>();
CreatedDate = DateTime.UtcNow;
LastModified = DateTime.UtcNow;
}
/// <summary>
/// Validates that the profile has at least one monitor configured
/// </summary>
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(Name) && MonitorSettings != null && MonitorSettings.Count > 0;
}
/// <summary>
/// Updates the last modified timestamp
/// </summary>
public void Touch()
{
LastModified = DateTime.UtcNow;
}
}
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Container for all PowerDisplay profiles
/// </summary>
public class PowerDisplayProfiles
{
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;
}
/// <summary>
/// Gets the profile by name
/// </summary>
public PowerDisplayProfile? GetProfile(string name)
{
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>
public void SetProfile(PowerDisplayProfile profile)
{
if (profile == null || !profile.IsValid())
{
throw new ArgumentException("Profile is invalid");
}
var existing = GetProfile(profile.Name);
if (existing != null)
{
Profiles.Remove(existing);
}
profile.Touch();
Profiles.Add(profile);
LastUpdated = DateTime.UtcNow;
}
/// <summary>
/// Removes a profile by name
/// </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;
}
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>
public bool IsNameAvailable(string name, string? excludeName = null)
{
if (string.IsNullOrWhiteSpace(name))
{
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)))
{
return false;
}
return true;
}
}
}

View File

@@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
BrightnessUpdateRate = "1s"; BrightnessUpdateRate = "1s";
Monitors = new List<MonitorInfo>(); Monitors = new List<MonitorInfo>();
RestoreSettingsOnStartup = true; RestoreSettingsOnStartup = true;
CurrentProfile = "Custom";
// Note: saved_monitor_settings has been moved to monitor_state.json // Note: saved_monitor_settings has been moved to monitor_state.json
// which is managed separately by PowerDisplay app // which is managed separately by PowerDisplay app
@@ -36,11 +37,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("restore_settings_on_startup")] [JsonPropertyName("restore_settings_on_startup")]
public bool RestoreSettingsOnStartup { get; set; } 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> /// <summary>
/// Pending color temperature operation from Settings UI. /// Pending color temperature operation from Settings UI.
/// This is cleared after PowerDisplay processes it. /// This is cleared after PowerDisplay processes it.
/// </summary> /// </summary>
[JsonPropertyName("pending_color_temperature_operation")] [JsonPropertyName("pending_color_temperature_operation")]
public ColorTemperatureOperation PendingColorTemperatureOperation { get; set; } public ColorTemperatureOperation PendingColorTemperatureOperation { get; set; }
/// <summary>
/// Pending profile operation from Settings UI.
/// This is cleared after PowerDisplay processes it.
/// </summary>
[JsonPropertyName("pending_profile_operation")]
public ProfileOperation PendingProfileOperation { get; set; }
} }
} }

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Monitor settings for a specific profile
/// </summary>
public class ProfileMonitorSetting
{
[JsonPropertyName("hardwareId")]
public string HardwareId { get; set; }
[JsonPropertyName("brightness")]
public int Brightness { get; set; }
[JsonPropertyName("contrast")]
public int? Contrast { get; set; }
[JsonPropertyName("volume")]
public int? Volume { get; set; }
[JsonPropertyName("colorTemperature")]
public int ColorTemperature { get; set; }
public ProfileMonitorSetting()
{
HardwareId = string.Empty;
Brightness = 100;
ColorTemperature = 6500;
}
public ProfileMonitorSetting(string hardwareId, int brightness, int colorTemperature, int? contrast = null, int? volume = null)
{
HardwareId = hardwareId;
Brightness = brightness;
ColorTemperature = colorTemperature;
Contrast = contrast;
Volume = volume;
}
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Represents a pending profile operation to be applied by PowerDisplay
/// </summary>
public class ProfileOperation
{
[JsonPropertyName("profileName")]
public string ProfileName { get; set; }
[JsonPropertyName("monitorSettings")]
public List<ProfileMonitorSetting> MonitorSettings { get; set; }
public ProfileOperation()
{
ProfileName = string.Empty;
MonitorSettings = new List<ProfileMonitorSetting>();
}
public ProfileOperation(string profileName, List<ProfileMonitorSetting> monitorSettings)
{
ProfileName = profileName;
MonitorSettings = monitorSettings ?? new List<ProfileMonitorSetting>();
}
}
}

View File

@@ -54,6 +54,47 @@
</controls:SettingsGroup> </controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Profiles_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_CurrentProfile"
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>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_ProfileStatus"
HeaderIcon="{ui:FontIcon Glyph=&#xE946;}">
<TextBlock
Text="{x:Bind ViewModel.CurrentProfile, Mode=OneWay}"
FontWeight="SemiBold"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}"> <controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<!-- Empty state hint --> <!-- Empty state hint -->
<InfoBar <InfoBar

View File

@@ -0,0 +1,158 @@
<ContentDialog
x:Class="Microsoft.PowerToys.Settings.UI.Views.ProfileEditorDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"
Title="Edit Profile"
PrimaryButtonText="Save"
CloseButtonText="Cancel"
DefaultButton="Primary"
IsPrimaryButtonEnabled="{x:Bind ViewModel.CanSave, Mode=OneWay}"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
CloseButtonClick="ContentDialog_CloseButtonClick">
<ScrollViewer MaxHeight="600" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="16">
<!-- Profile Name -->
<StackPanel Spacing="4">
<TextBlock
Text="Profile Name"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<TextBox
x:Name="ProfileNameTextBox"
Text="{x:Bind ViewModel.ProfileName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="Enter profile name (e.g., 'Work Setup', 'Gaming')"
MaxLength="50" />
<TextBlock
Text="Leave empty to auto-generate (Profile1, Profile2, etc.)"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
<!-- Monitors Selection -->
<StackPanel Spacing="8">
<TextBlock
Text="Select Monitors to Include"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<TextBlock
Text="At least one monitor must be selected"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Visibility="{x:Bind ViewModel.HasSelectedMonitors, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}" />
<ItemsControl ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="viewmodels:MonitorSelectionItem">
<Expander
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsExpanded="{x:Bind IsSelected, Mode=TwoWay}"
Margin="0,0,0,8">
<Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox
IsChecked="{x:Bind IsSelected, Mode=TwoWay}"
Margin="0,0,12,0" />
<StackPanel Grid.Column="1" Orientation="Vertical">
<TextBlock
Text="{x:Bind Monitor.Name}"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<TextBlock
Text="{x:Bind Monitor.HardwareId}"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Expander.Header>
<StackPanel Spacing="12" Padding="32,8,8,8">
<!-- Brightness -->
<StackPanel Spacing="4">
<Grid>
<TextBlock Text="Brightness" />
<TextBlock
Text="{x:Bind Brightness, Mode=OneWay}"
HorizontalAlignment="Right"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
</Grid>
<Slider
Value="{x:Bind Brightness, Mode=TwoWay}"
Minimum="0"
Maximum="100"
StepFrequency="1"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}" />
</StackPanel>
<!-- Color Temperature -->
<StackPanel
Spacing="4"
Visibility="{x:Bind SupportsColorTemperature, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<TextBlock Text="Color Temperature" />
<TextBlock
Text="{x:Bind ColorTemperature, Mode=OneWay}"
HorizontalAlignment="Right"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
</Grid>
<Slider
Value="{x:Bind ColorTemperature, Mode=TwoWay}"
Minimum="4000"
Maximum="10000"
StepFrequency="100"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}" />
</StackPanel>
<!-- Contrast -->
<StackPanel
Spacing="4"
Visibility="{x:Bind SupportsContrast, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<TextBlock Text="Contrast" />
<TextBlock
Text="{x:Bind Contrast, Mode=OneWay}"
HorizontalAlignment="Right"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
</Grid>
<Slider
Value="{x:Bind Contrast, Mode=TwoWay}"
Minimum="0"
Maximum="100"
StepFrequency="1"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}" />
</StackPanel>
<!-- Volume -->
<StackPanel
Spacing="4"
Visibility="{x:Bind SupportsVolume, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<TextBlock Text="Volume" />
<TextBlock
Text="{x:Bind Volume, Mode=OneWay}"
HorizontalAlignment="Right"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
</Grid>
<Slider
Value="{x:Bind Volume, Mode=TwoWay}"
Minimum="0"
Maximum="100"
StepFrequency="1"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</ScrollViewer>
</ContentDialog>

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Collections.ObjectModel;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
/// <summary>
/// Dialog for creating/editing PowerDisplay profiles
/// </summary>
public sealed partial class ProfileEditorDialog : ContentDialog
{
public ProfileEditorViewModel ViewModel { get; private set; }
public PowerDisplayProfile? ResultProfile { get; private set; }
public ProfileEditorDialog(ObservableCollection<MonitorInfo> availableMonitors, string defaultName = "")
{
this.InitializeComponent();
ViewModel = new ProfileEditorViewModel(availableMonitors, defaultName);
}
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (ViewModel.CanSave)
{
ResultProfile = ViewModel.CreateProfile();
}
}
private void ContentDialog_CloseButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
ResultProfile = null;
}
}
}

View File

@@ -5547,6 +5547,30 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="PowerDisplay_BrightnessUpdateRate.Description" xml:space="preserve"> <data name="PowerDisplay_BrightnessUpdateRate.Description" xml:space="preserve">
<value>How frequently to update brightness values</value> <value>How frequently to update brightness values</value>
</data> </data>
<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>
<data name="PowerDisplay_CurrentProfile.Description" xml:space="preserve">
<value>Select a profile to apply predefined monitor settings</value>
</data>
<data name="PowerDisplay_AddProfile_Button.ToolTipService.ToolTip" xml:space="preserve">
<value>Add new profile</value>
</data>
<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"> <data name="PowerDisplay_Monitor_HideMonitor.Header" xml:space="preserve">
<value>Hide monitor</value> <value>Hide monitor</value>
</data> </data>

View File

@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
/// <summary>
/// ViewModel for monitor selection in profile editor
/// </summary>
public class MonitorSelectionItem : INotifyPropertyChanged
{
private bool _isSelected;
private int _brightness = 100;
private int _contrast = 50;
private int _volume = 50;
private int _colorTemperature = 6500;
public required MonitorInfo Monitor { get; set; }
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged();
}
}
}
public int Brightness
{
get => _brightness;
set
{
if (_brightness != value)
{
_brightness = value;
OnPropertyChanged();
}
}
}
public int Contrast
{
get => _contrast;
set
{
if (_contrast != value)
{
_contrast = value;
OnPropertyChanged();
}
}
}
public int Volume
{
get => _volume;
set
{
if (_volume != value)
{
_volume = value;
OnPropertyChanged();
}
}
}
public int ColorTemperature
{
get => _colorTemperature;
set
{
if (_colorTemperature != value)
{
_colorTemperature = value;
OnPropertyChanged();
}
}
}
public bool SupportsContrast => Monitor?.SupportsContrast ?? false;
public bool SupportsVolume => Monitor?.SupportsVolume ?? false;
public bool SupportsColorTemperature => Monitor?.SupportsColorTemperature ?? false;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class PowerDisplayViewModel : PageViewModelBase public partial class PowerDisplayViewModel : PageViewModelBase
{ {
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true };
protected override string ModuleName => PowerDisplaySettings.ModuleName; protected override string ModuleName => PowerDisplaySettings.ModuleName;
private GeneralSettings GeneralSettingsConfig { get; set; } 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. // set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc; SendConfigMSG = ipcMSGCallBackFunc;
// Load profiles
LoadProfiles();
// Listen for monitor refresh events from PowerDisplay.exe // Listen for monitor refresh events from PowerDisplay.exe
NativeEventWaiter.WaitForEventLoop( NativeEventWaiter.WaitForEventLoop(
Constants.RefreshPowerDisplayMonitorsEvent(), Constants.RefreshPowerDisplayMonitorsEvent(),
@@ -469,6 +474,83 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _hasMonitors; private bool _hasMonitors;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); 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() public void RefreshEnabledState()
{ {
InitializeEnabledValue(); InitializeEnabledValue();
@@ -488,6 +570,323 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return true; 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() private void NotifySettingsChanged()
{ {
// Persist locally first so settings survive even if the module DLL isn't loaded yet. // Persist locally first so settings survive even if the module DLL isn't loaded yet.

View File

@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
/// <summary>
/// ViewModel for Profile Editor Dialog
/// </summary>
public class ProfileEditorViewModel : INotifyPropertyChanged
{
private string _profileName = string.Empty;
private ObservableCollection<MonitorSelectionItem> _monitors;
public ProfileEditorViewModel(ObservableCollection<MonitorInfo> availableMonitors, string defaultName = "")
{
_profileName = defaultName;
_monitors = new ObservableCollection<MonitorSelectionItem>();
// Initialize monitor selection items
foreach (var monitor in availableMonitors)
{
var item = new MonitorSelectionItem
{
Monitor = monitor,
IsSelected = false,
Brightness = monitor.CurrentBrightness,
ColorTemperature = monitor.ColorTemperature,
};
// Subscribe to selection changes
item.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MonitorSelectionItem.IsSelected))
{
OnPropertyChanged(nameof(CanSave));
OnPropertyChanged(nameof(HasSelectedMonitors));
}
};
_monitors.Add(item);
}
}
public string ProfileName
{
get => _profileName;
set
{
if (_profileName != value)
{
_profileName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CanSave));
}
}
}
public ObservableCollection<MonitorSelectionItem> Monitors
{
get => _monitors;
set
{
if (_monitors != value)
{
_monitors = value;
OnPropertyChanged();
}
}
}
public bool HasSelectedMonitors => _monitors?.Any(m => m.IsSelected) ?? false;
public bool CanSave => !string.IsNullOrWhiteSpace(_profileName) && HasSelectedMonitors;
public PowerDisplayProfile CreateProfile()
{
var settings = _monitors
.Where(m => m.IsSelected)
.Select(m => new ProfileMonitorSetting(
m.Monitor.HardwareId,
m.Brightness,
m.ColorTemperature,
m.SupportsContrast ? (int?)m.Contrast : null,
m.SupportsVolume ? (int?)m.Volume : null))
.ToList();
return new PowerDisplayProfile(_profileName, settings);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}