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;
|
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 TerminatePowerDisplayEvent();
|
||||||
static hstring RefreshPowerDisplayMonitorsEvent();
|
static hstring RefreshPowerDisplayMonitorsEvent();
|
||||||
static hstring SettingsUpdatedPowerDisplayEvent();
|
static hstring SettingsUpdatedPowerDisplayEvent();
|
||||||
|
static hstring ApplyColorTemperaturePowerDisplayEvent();
|
||||||
|
static hstring ApplyProfilePowerDisplayEvent();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
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 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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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&)
|
||||||
{
|
{
|
||||||
|
|||||||
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";
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
|
|
||||||
|
<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}">
|
<controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||||
<!-- Empty state hint -->
|
<!-- Empty state hint -->
|
||||||
<InfoBar
|
<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">
|
<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>
|
||||||
|
|||||||
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
|
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.
|
||||||
|
|||||||
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