mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
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:
@@ -211,4 +211,12 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring TerminatePowerDisplayEvent();
|
||||
static hstring RefreshPowerDisplayMonitorsEvent();
|
||||
static hstring SettingsUpdatedPowerDisplayEvent();
|
||||
static hstring ApplyColorTemperaturePowerDisplayEvent();
|
||||
static hstring ApplyProfilePowerDisplayEvent();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ namespace PowerToys
|
||||
static String TerminatePowerDisplayEvent();
|
||||
static String RefreshPowerDisplayMonitorsEvent();
|
||||
static String SettingsUpdatedPowerDisplayEvent();
|
||||
static String ApplyColorTemperaturePowerDisplayEvent();
|
||||
static String ApplyProfilePowerDisplayEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 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_PROFILE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
|
||||
|
||||
// used from quick access window
|
||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||
|
||||
227
src/modules/powerdisplay/PowerDisplay/Helpers/ProfileManager.cs
Normal file
227
src/modules/powerdisplay/PowerDisplay/Helpers/ProfileManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace PowerDisplay
|
||||
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 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 int _powerToysRunnerPid;
|
||||
@@ -176,6 +177,21 @@ namespace PowerDisplay
|
||||
},
|
||||
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)
|
||||
if (_powerToysRunnerPid > 0)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,10 @@ namespace PowerDisplay.Serialization
|
||||
[JsonSerializable(typeof(MonitorStateEntry))]
|
||||
[JsonSerializable(typeof(PowerDisplaySettings))]
|
||||
[JsonSerializable(typeof(ColorTemperatureOperation))]
|
||||
[JsonSerializable(typeof(ProfileOperation))]
|
||||
[JsonSerializable(typeof(PowerDisplayProfiles))]
|
||||
[JsonSerializable(typeof(PowerDisplayProfile))]
|
||||
[JsonSerializable(typeof(ProfileMonitorSetting))]
|
||||
|
||||
// MonitorInfo and related types (Settings.UI.Library)
|
||||
[JsonSerializable(typeof(MonitorInfo))]
|
||||
@@ -33,6 +37,8 @@ namespace PowerDisplay.Serialization
|
||||
[JsonSerializable(typeof(List<MonitorInfo>))]
|
||||
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
|
||||
[JsonSerializable(typeof(List<VcpValueInfo>))]
|
||||
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
|
||||
[JsonSerializable(typeof(List<ProfileMonitorSetting>))]
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = true,
|
||||
|
||||
@@ -38,6 +38,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly MonitorStateManager _stateManager;
|
||||
private readonly ProfileManager _profileManager;
|
||||
|
||||
private ObservableCollection<MonitorViewModel> _monitors;
|
||||
private string _statusText;
|
||||
@@ -61,6 +62,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
// Initialize settings utils
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_stateManager = new MonitorStateManager();
|
||||
_profileManager = new ProfileManager();
|
||||
|
||||
// Initialize the monitor manager
|
||||
_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>
|
||||
/// Apply hardware parameter changes (brightness, color temperature)
|
||||
/// Asynchronous operation that communicates with monitor hardware via DDC/CI
|
||||
@@ -637,6 +821,35 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.Properties.RestoreSettingsOnStartup)
|
||||
{
|
||||
// Restore saved settings from configuration file
|
||||
|
||||
@@ -77,7 +77,8 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
/// </summary>
|
||||
/// <param name="brightness">Brightness value (0-100)</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);
|
||||
|
||||
@@ -91,20 +92,20 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
// Apply to hardware (with or without debounce)
|
||||
if (immediate)
|
||||
{
|
||||
await ApplyBrightnessToHardwareAsync(brightness);
|
||||
await ApplyBrightnessToHardwareAsync(brightness, fromProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Debounce for slider smoothness
|
||||
// Debounce for slider smoothness (always from user interaction, not from profile)
|
||||
var capturedValue = brightness;
|
||||
_brightnessDebouncer.Debounce(async () => await ApplyBrightnessToHardwareAsync(capturedValue));
|
||||
_brightnessDebouncer.Debounce(async () => await ApplyBrightnessToHardwareAsync(capturedValue, fromUserInteraction: true));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply contrast with hardware update and state persistence.
|
||||
/// </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);
|
||||
|
||||
@@ -117,19 +118,19 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
await ApplyContrastToHardwareAsync(contrast);
|
||||
await ApplyContrastToHardwareAsync(contrast, fromProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
var capturedValue = contrast;
|
||||
_contrastDebouncer.Debounce(async () => await ApplyContrastToHardwareAsync(capturedValue));
|
||||
_contrastDebouncer.Debounce(async () => await ApplyContrastToHardwareAsync(capturedValue, fromUserInteraction: true));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply volume with hardware update and state persistence.
|
||||
/// </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);
|
||||
|
||||
@@ -141,12 +142,12 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
await ApplyVolumeToHardwareAsync(volume);
|
||||
await ApplyVolumeToHardwareAsync(volume, fromProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
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.
|
||||
/// Always immediate (no debouncing for discrete preset values).
|
||||
/// </summary>
|
||||
public async Task SetColorTemperatureAsync(int colorTemperature)
|
||||
public async Task SetColorTemperatureAsync(int colorTemperature, bool fromProfile = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -168,7 +169,14 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
OnPropertyChanged(nameof(ColorTemperature));
|
||||
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");
|
||||
}
|
||||
else
|
||||
@@ -186,7 +194,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
/// Internal method - applies brightness to hardware and persists state.
|
||||
/// Unified logic for all sources (Flyout, Settings, etc.).
|
||||
/// </summary>
|
||||
private async Task ApplyBrightnessToHardwareAsync(int brightness)
|
||||
private async Task ApplyBrightnessToHardwareAsync(int brightness, bool fromUserInteraction = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -196,7 +204,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
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
|
||||
{
|
||||
@@ -212,7 +226,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
/// <summary>
|
||||
/// Internal method - applies contrast to hardware and persists state.
|
||||
/// </summary>
|
||||
private async Task ApplyContrastToHardwareAsync(int contrast)
|
||||
private async Task ApplyContrastToHardwareAsync(int contrast, bool fromUserInteraction = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -222,7 +236,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
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
|
||||
{
|
||||
@@ -238,7 +258,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
/// <summary>
|
||||
/// Internal method - applies volume to hardware and persists state.
|
||||
/// </summary>
|
||||
private async Task ApplyVolumeToHardwareAsync(int volume)
|
||||
private async Task ApplyVolumeToHardwareAsync(int volume, bool fromUserInteraction = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -248,7 +268,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -62,6 +62,7 @@ private:
|
||||
HANDLE m_hRefreshEvent = nullptr;
|
||||
HANDLE m_hSettingsUpdatedEvent = nullptr;
|
||||
HANDLE m_hApplyColorTemperatureEvent = nullptr;
|
||||
HANDLE m_hApplyProfileEvent = nullptr;
|
||||
|
||||
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
|
||||
{
|
||||
@@ -199,8 +200,9 @@ public:
|
||||
m_hRefreshEvent = CreateDefaultEvent(CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT);
|
||||
m_hSettingsUpdatedEvent = CreateDefaultEvent(CommonSharedConstants::SETTINGS_UPDATED_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");
|
||||
}
|
||||
@@ -244,6 +246,11 @@ public:
|
||||
CloseHandle(m_hApplyColorTemperatureEvent);
|
||||
m_hApplyColorTemperatureEvent = nullptr;
|
||||
}
|
||||
if (m_hApplyProfileEvent)
|
||||
{
|
||||
CloseHandle(m_hApplyProfileEvent);
|
||||
m_hApplyProfileEvent = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
virtual void destroy() override
|
||||
@@ -327,6 +334,19 @@ public:
|
||||
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&)
|
||||
{
|
||||
|
||||
61
src/settings-ui/Settings.UI.Library/PowerDisplayProfile.cs
Normal file
61
src/settings-ui/Settings.UI.Library/PowerDisplayProfile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/settings-ui/Settings.UI.Library/PowerDisplayProfiles.cs
Normal file
147
src/settings-ui/Settings.UI.Library/PowerDisplayProfiles.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ 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
|
||||
@@ -36,11 +37,24 @@ 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.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pending_color_temperature_operation")]
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
45
src/settings-ui/Settings.UI.Library/ProfileMonitorSetting.cs
Normal file
45
src/settings-ui/Settings.UI.Library/ProfileMonitorSetting.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/settings-ui/Settings.UI.Library/ProfileOperation.cs
Normal file
33
src/settings-ui/Settings.UI.Library/ProfileOperation.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,47 @@
|
||||
|
||||
</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=}">
|
||||
<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="" 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="" FontSize="16" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Uid="PowerDisplay_SaveAsProfile_Button"
|
||||
Command="{x:Bind ViewModel.SaveAsProfileCommand}"
|
||||
ToolTipService.ToolTip="Save current settings as new profile">
|
||||
<FontIcon Glyph="" FontSize="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_ProfileStatus"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<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}">
|
||||
<!-- Empty state hint -->
|
||||
<InfoBar
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
<value>How frequently to update brightness values</value>
|
||||
</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">
|
||||
<value>Hide monitor</value>
|
||||
</data>
|
||||
|
||||
104
src/settings-ui/Settings.UI/ViewModels/MonitorSelectionItem.cs
Normal file
104
src/settings-ui/Settings.UI/ViewModels/MonitorSelectionItem.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class PowerDisplayViewModel : PageViewModelBase
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true };
|
||||
|
||||
protected override string ModuleName => PowerDisplaySettings.ModuleName;
|
||||
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
@@ -66,6 +68,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
// Load profiles
|
||||
LoadProfiles();
|
||||
|
||||
// Listen for monitor refresh events from PowerDisplay.exe
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
Constants.RefreshPowerDisplayMonitorsEvent(),
|
||||
@@ -469,6 +474,83 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
private bool _hasMonitors;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Profile-related fields
|
||||
private ObservableCollection<string> _profiles = new ObservableCollection<string>();
|
||||
private string _selectedProfile = PowerDisplayProfiles.CustomProfileName;
|
||||
private string _currentProfile = PowerDisplayProfiles.CustomProfileName;
|
||||
private string _profilesFilePath = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of available profile names (including Custom)
|
||||
/// </summary>
|
||||
public ObservableCollection<string> Profiles
|
||||
{
|
||||
get => _profiles;
|
||||
set
|
||||
{
|
||||
if (_profiles != value)
|
||||
{
|
||||
_profiles = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Currently selected profile in the ComboBox
|
||||
/// </summary>
|
||||
public string SelectedProfile
|
||||
{
|
||||
get => _selectedProfile;
|
||||
set
|
||||
{
|
||||
if (_selectedProfile != value && !string.IsNullOrEmpty(value))
|
||||
{
|
||||
_selectedProfile = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
// Apply the selected profile
|
||||
ApplyProfile(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Currently active profile (read from settings, may differ from selected during transition)
|
||||
/// </summary>
|
||||
public string CurrentProfile
|
||||
{
|
||||
get => _currentProfile;
|
||||
set
|
||||
{
|
||||
if (_currentProfile != value)
|
||||
{
|
||||
_currentProfile = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsCustomProfile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if current profile is Custom
|
||||
/// </summary>
|
||||
public bool IsCustomProfile => _currentProfile?.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase) ?? true;
|
||||
|
||||
/// <summary>
|
||||
/// True if a non-Custom profile is selected (enables delete/rename)
|
||||
/// </summary>
|
||||
public bool CanModifySelectedProfile => !string.IsNullOrEmpty(_selectedProfile) &&
|
||||
!_selectedProfile.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public ButtonClickCommand AddProfileCommand => new ButtonClickCommand(AddProfile);
|
||||
|
||||
public ButtonClickCommand DeleteProfileCommand => new ButtonClickCommand(DeleteProfile);
|
||||
|
||||
public ButtonClickCommand RenameProfileCommand => new ButtonClickCommand(RenameProfile);
|
||||
|
||||
public ButtonClickCommand SaveAsProfileCommand => new ButtonClickCommand(SaveAsProfile);
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
InitializeEnabledValue();
|
||||
@@ -488,6 +570,323 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load profiles from disk
|
||||
/// </summary>
|
||||
private void LoadProfiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
|
||||
_profilesFilePath = Path.Combine(powerToysPath, "profiles.json");
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
|
||||
// Build profile names list
|
||||
var profileNames = new List<string> { PowerDisplayProfiles.CustomProfileName };
|
||||
profileNames.AddRange(profilesData.Profiles.Select(p => p.Name));
|
||||
|
||||
Profiles = new ObservableCollection<string>(profileNames);
|
||||
|
||||
// Set current profile from settings
|
||||
CurrentProfile = _settings.Properties.CurrentProfile ?? PowerDisplayProfiles.CustomProfileName;
|
||||
_selectedProfile = CurrentProfile;
|
||||
OnPropertyChanged(nameof(SelectedProfile));
|
||||
|
||||
Logger.LogInfo($"Loaded {profilesData.Profiles.Count} profiles, current: {CurrentProfile}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load profiles: {ex.Message}");
|
||||
Profiles = new ObservableCollection<string> { PowerDisplayProfiles.CustomProfileName };
|
||||
CurrentProfile = PowerDisplayProfiles.CustomProfileName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load profiles data from disk
|
||||
/// </summary>
|
||||
private PowerDisplayProfiles LoadProfilesFromDisk()
|
||||
{
|
||||
if (File.Exists(_profilesFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(_profilesFilePath);
|
||||
var profiles = JsonSerializer.Deserialize<PowerDisplayProfiles>(json);
|
||||
return profiles ?? new PowerDisplayProfiles();
|
||||
}
|
||||
|
||||
return new PowerDisplayProfiles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save profiles data to disk
|
||||
/// </summary>
|
||||
private void SaveProfilesToDisk(PowerDisplayProfiles profiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
profiles.LastUpdated = DateTime.UtcNow;
|
||||
var json = JsonSerializer.Serialize(profiles, _jsonSerializerOptions);
|
||||
File.WriteAllText(_profilesFilePath, json);
|
||||
Logger.LogInfo($"Saved profiles to disk: {_profilesFilePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save profiles: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a profile
|
||||
/// </summary>
|
||||
private void ApplyProfile(string profileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"Applying profile: {profileName}");
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
var profile = profilesData.GetProfile(profileName);
|
||||
|
||||
if (profile == null || !profile.IsValid())
|
||||
{
|
||||
Logger.LogWarning($"Profile '{profileName}' not found or invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create pending operation
|
||||
var operation = new ProfileOperation(profileName, profile.MonitorSettings);
|
||||
_settings.Properties.PendingProfileOperation = operation;
|
||||
_settings.Properties.CurrentProfile = profileName;
|
||||
|
||||
// Save settings
|
||||
NotifySettingsChanged();
|
||||
|
||||
// Update current profile
|
||||
CurrentProfile = profileName;
|
||||
|
||||
// Send custom action to trigger profile application
|
||||
SendConfigMSG(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{{ \"action\": {{ \"PowerDisplay\": {{ \"action_name\": \"ApplyProfile\", \"value\": \"{0}\" }} }} }}",
|
||||
profileName));
|
||||
|
||||
// Signal PowerDisplay to apply profile
|
||||
using (var eventHandle = new System.Threading.EventWaitHandle(
|
||||
false,
|
||||
System.Threading.EventResetMode.AutoReset,
|
||||
Constants.ApplyProfilePowerDisplayEvent()))
|
||||
{
|
||||
eventHandle.Set();
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Profile '{profileName}' applied successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to apply profile: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new profile
|
||||
/// </summary>
|
||||
private async void AddProfile()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("Adding new profile");
|
||||
|
||||
if (Monitors == null || Monitors.Count == 0)
|
||||
{
|
||||
Logger.LogWarning("No monitors available to create profile");
|
||||
return;
|
||||
}
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
var defaultName = profilesData.GenerateProfileName();
|
||||
|
||||
// Show profile editor dialog
|
||||
var dialog = new Views.ProfileEditorDialog(Monitors, defaultName);
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary && dialog.ResultProfile != null)
|
||||
{
|
||||
var newProfile = dialog.ResultProfile;
|
||||
|
||||
// Validate profile name
|
||||
if (string.IsNullOrWhiteSpace(newProfile.Name))
|
||||
{
|
||||
newProfile = new PowerDisplayProfile(defaultName, newProfile.MonitorSettings);
|
||||
}
|
||||
|
||||
profilesData.SetProfile(newProfile);
|
||||
SaveProfilesToDisk(profilesData);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
|
||||
Logger.LogInfo($"Profile '{newProfile.Name}' created successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to add profile: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the selected profile
|
||||
/// </summary>
|
||||
private void DeleteProfile()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!CanModifySelectedProfile)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Deleting profile: {SelectedProfile}");
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
profilesData.RemoveProfile(SelectedProfile);
|
||||
SaveProfilesToDisk(profilesData);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
|
||||
Logger.LogInfo($"Profile '{SelectedProfile}' deleted successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to delete profile: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rename the selected profile
|
||||
/// </summary>
|
||||
private async void RenameProfile()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!CanModifySelectedProfile)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Renaming profile: {SelectedProfile}");
|
||||
|
||||
// Load the existing profile
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
var existingProfile = profilesData.GetProfile(SelectedProfile);
|
||||
if (existingProfile == null)
|
||||
{
|
||||
Logger.LogWarning($"Profile '{SelectedProfile}' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Show profile editor dialog with existing profile data
|
||||
var dialog = new Views.ProfileEditorDialog(Monitors, existingProfile.Name);
|
||||
|
||||
// Pre-fill monitor settings from existing profile
|
||||
foreach (var monitorSetting in existingProfile.MonitorSettings)
|
||||
{
|
||||
var monitorItem = dialog.ViewModel.Monitors.FirstOrDefault(m => m.Monitor.HardwareId == monitorSetting.HardwareId);
|
||||
if (monitorItem != null)
|
||||
{
|
||||
monitorItem.IsSelected = true;
|
||||
monitorItem.Brightness = monitorSetting.Brightness;
|
||||
monitorItem.ColorTemperature = monitorSetting.ColorTemperature;
|
||||
if (monitorSetting.Contrast.HasValue)
|
||||
{
|
||||
monitorItem.Contrast = monitorSetting.Contrast.Value;
|
||||
}
|
||||
|
||||
if (monitorSetting.Volume.HasValue)
|
||||
{
|
||||
monitorItem.Volume = monitorSetting.Volume.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary && dialog.ResultProfile != null)
|
||||
{
|
||||
var updatedProfile = dialog.ResultProfile;
|
||||
|
||||
// Remove old profile and add updated one
|
||||
profilesData.RemoveProfile(SelectedProfile);
|
||||
profilesData.SetProfile(updatedProfile);
|
||||
SaveProfilesToDisk(profilesData);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
|
||||
// Select the renamed profile
|
||||
SelectedProfile = updatedProfile.Name;
|
||||
|
||||
Logger.LogInfo($"Profile renamed to '{updatedProfile.Name}' successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to rename profile: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current settings as a new profile
|
||||
/// </summary>
|
||||
private void SaveAsProfile()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("Saving current settings as new profile");
|
||||
|
||||
var profilesData = LoadProfilesFromDisk();
|
||||
var newProfileName = profilesData.GenerateProfileName();
|
||||
|
||||
// Collect current monitor settings
|
||||
var monitorSettings = new List<ProfileMonitorSetting>();
|
||||
foreach (var monitor in Monitors)
|
||||
{
|
||||
var setting = new ProfileMonitorSetting(
|
||||
monitor.HardwareId,
|
||||
monitor.CurrentBrightness,
|
||||
monitor.ColorTemperature,
|
||||
monitor.EnableContrast ? (int?)50 : null,
|
||||
monitor.EnableVolume ? (int?)50 : null);
|
||||
|
||||
monitorSettings.Add(setting);
|
||||
}
|
||||
|
||||
if (monitorSettings.Count == 0)
|
||||
{
|
||||
Logger.LogWarning("No monitors available to save profile");
|
||||
return;
|
||||
}
|
||||
|
||||
var newProfile = new PowerDisplayProfile(newProfileName, monitorSettings);
|
||||
profilesData.SetProfile(newProfile);
|
||||
SaveProfilesToDisk(profilesData);
|
||||
|
||||
// Reload profile list and select the new profile
|
||||
LoadProfiles();
|
||||
SelectedProfile = newProfileName;
|
||||
|
||||
Logger.LogInfo($"Saved as profile '{newProfileName}' successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save as profile: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
{
|
||||
// Persist locally first so settings survive even if the module DLL isn't loaded yet.
|
||||
|
||||
106
src/settings-ui/Settings.UI/ViewModels/ProfileEditorViewModel.cs
Normal file
106
src/settings-ui/Settings.UI/ViewModels/ProfileEditorViewModel.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user