mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request Introduce a new PowerToys' module PowerDisplay to let user can control their monitor settings without touching monitor's button. Support feature list: Common: 1. Profiles support 2. Integration with LightSwitch (auto switch profile when theme change) 3. TrayIcon 4. Save and restore settings when startup 5. Shortcut 6. Rotation 7. GPO support 8. Auto re-discovery monitor when plugging and unplugging monitors. 9. Identify Monitors 10. Quick profile switch Especially for DDC/CI monitor: 1. Brightness 2. Contrast 3. Volume 4. Color temperature (preset profile) 5. Input source 6. Power State (poweroff) Design doc: https://github.com/microsoft/PowerToys/blob/yuleng/display/pr/3/doc/devdocs/modules/powerdisplay/design.md AOT compatibility: I designed this module for AOT from the start, so I'm pretty sure at least 95% of it is AOT compatible. But unfortunately, PowerToys still have a AOT blocker to block this module publish with AOT. Currently PowerToys will check the .net file version (file version not lib version) to avoid crash. So, all modules should reference Common.UI or add UseWPF to avoid overwrite the .net file with different version (which may cause crash). Todo: - [ ] BugBash - [ ] Icon - [ ] IdentifyWindow UI improvement Demo Main UI: <img width="546" height="671" alt="image" src="https://github.com/user-attachments/assets/b0ad9ac5-8000-4365-a192-ab8c2d66d4f1" /> Input Source: <img width="536" height="674" alt="image" src="https://github.com/user-attachments/assets/80f9ccd7-4f8c-4201-b177-cc86c5bcc9e3" /> Settings UI: <img width="1581" height="1191" alt="image" src="https://github.com/user-attachments/assets/6a82e4bb-8f96-4f28-abf9-d7c45e1c8ef7" /> <img width="1525" height="1146" alt="image" src="https://github.com/user-attachments/assets/aae81e65-08fd-453a-bf52-02a74f2fdea0" /> Closes: #42942 #42678 #41117 #38109 #35564 #34932 #28500 #1052 #18149 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #1052 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Co-authored-by: Yu Leng <yuleng@microsoft.com> Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: moooyo <lengyuchn@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
870 lines
27 KiB
C#
870 lines
27 KiB
C#
// 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.ComponentModel;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using ManagedCommon;
|
|
using Microsoft.UI.Xaml;
|
|
|
|
using PowerDisplay.Common.Models;
|
|
using PowerDisplay.Configuration;
|
|
using PowerDisplay.Helpers;
|
|
using Monitor = PowerDisplay.Common.Models.Monitor;
|
|
|
|
namespace PowerDisplay.ViewModels;
|
|
|
|
/// <summary>
|
|
/// ViewModel for individual monitor
|
|
/// </summary>
|
|
public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
|
{
|
|
private readonly Monitor _monitor;
|
|
private readonly MonitorManager _monitorManager;
|
|
private readonly MainViewModel? _mainViewModel;
|
|
|
|
private int _brightness;
|
|
private int _contrast;
|
|
private int _volume;
|
|
private bool _isAvailable;
|
|
|
|
// Visibility settings (controlled by Settings UI)
|
|
private bool _showContrast;
|
|
private bool _showVolume;
|
|
private bool _showInputSource;
|
|
private bool _showRotation;
|
|
private bool _showPowerState;
|
|
|
|
/// <summary>
|
|
/// Updates a property value directly without triggering hardware updates.
|
|
/// Used during initialization to update UI from saved state.
|
|
/// </summary>
|
|
internal void UpdatePropertySilently(string propertyName, int value)
|
|
{
|
|
switch (propertyName)
|
|
{
|
|
case nameof(Brightness):
|
|
_brightness = value;
|
|
OnPropertyChanged(nameof(Brightness));
|
|
break;
|
|
case nameof(Contrast):
|
|
_contrast = value;
|
|
OnPropertyChanged(nameof(Contrast));
|
|
OnPropertyChanged(nameof(ContrastPercent));
|
|
break;
|
|
case nameof(Volume):
|
|
_volume = value;
|
|
OnPropertyChanged(nameof(Volume));
|
|
break;
|
|
case nameof(ColorTemperature):
|
|
// Update underlying monitor model
|
|
_monitor.CurrentColorTemperature = value;
|
|
OnPropertyChanged(nameof(ColorTemperature));
|
|
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply brightness with hardware update and state persistence.
|
|
/// </summary>
|
|
/// <param name="brightness">Brightness value (0-100)</param>
|
|
public async Task SetBrightnessAsync(int brightness)
|
|
{
|
|
brightness = Math.Clamp(brightness, MinBrightness, MaxBrightness);
|
|
|
|
// Update UI state immediately
|
|
if (_brightness != brightness)
|
|
{
|
|
_brightness = brightness;
|
|
OnPropertyChanged(nameof(Brightness));
|
|
}
|
|
|
|
// Apply to hardware
|
|
await ApplyPropertyToHardwareAsync(nameof(Brightness), brightness, _monitorManager.SetBrightnessAsync);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply contrast with hardware update and state persistence.
|
|
/// </summary>
|
|
public async Task SetContrastAsync(int contrast)
|
|
{
|
|
contrast = Math.Clamp(contrast, MinContrast, MaxContrast);
|
|
|
|
if (_contrast != contrast)
|
|
{
|
|
_contrast = contrast;
|
|
OnPropertyChanged(nameof(Contrast));
|
|
OnPropertyChanged(nameof(ContrastPercent));
|
|
}
|
|
|
|
await ApplyPropertyToHardwareAsync(nameof(Contrast), contrast, _monitorManager.SetContrastAsync);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply volume with hardware update and state persistence.
|
|
/// </summary>
|
|
public async Task SetVolumeAsync(int volume)
|
|
{
|
|
volume = Math.Clamp(volume, MinVolume, MaxVolume);
|
|
|
|
if (_volume != volume)
|
|
{
|
|
_volume = volume;
|
|
OnPropertyChanged(nameof(Volume));
|
|
}
|
|
|
|
await ApplyPropertyToHardwareAsync(nameof(Volume), volume, _monitorManager.SetVolumeAsync);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
{
|
|
try
|
|
{
|
|
var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature);
|
|
|
|
if (result.IsSuccess)
|
|
{
|
|
_monitor.CurrentColorTemperature = colorTemperature;
|
|
OnPropertyChanged(nameof(ColorTemperature));
|
|
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
|
|
|
// Refresh the color presets list to update IsSelected checkmarks in UI
|
|
RefreshAvailableColorPresets();
|
|
|
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.Id, nameof(ColorTemperature), colorTemperature);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"[{Id}] Exception setting color temperature: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generic method to apply a monitor property to hardware and persist state.
|
|
/// Consolidates common logic for brightness, contrast, and volume operations.
|
|
/// </summary>
|
|
/// <param name="propertyName">Name of the property being set (for logging and state persistence)</param>
|
|
/// <param name="value">Value to apply</param>
|
|
/// <param name="setAsyncFunc">Async function to call on MonitorManager</param>
|
|
private async Task ApplyPropertyToHardwareAsync(
|
|
string propertyName,
|
|
int value,
|
|
Func<string, int, CancellationToken, Task<MonitorOperationResult>> setAsyncFunc)
|
|
{
|
|
try
|
|
{
|
|
var result = await setAsyncFunc(Id, value, default);
|
|
|
|
if (result.IsSuccess)
|
|
{
|
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.Id, propertyName, value);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning($"[{Id}] Failed to set {propertyName.ToLowerInvariant()}: {result.ErrorMessage}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"[{Id}] Exception setting {propertyName.ToLowerInvariant()}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Property to access IsInteractionEnabled from parent ViewModel
|
|
public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true;
|
|
|
|
public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
|
|
{
|
|
_monitor = monitor;
|
|
_monitorManager = monitorManager;
|
|
_mainViewModel = mainViewModel;
|
|
|
|
// Subscribe to MainViewModel property changes to update IsInteractionEnabled
|
|
if (_mainViewModel != null)
|
|
{
|
|
_mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged;
|
|
}
|
|
|
|
// Subscribe to underlying Monitor property changes (e.g., Orientation updates in mirror mode)
|
|
_monitor.PropertyChanged += OnMonitorPropertyChanged;
|
|
|
|
// Initialize Show properties based on hardware capabilities
|
|
_showContrast = monitor.SupportsContrast;
|
|
_showVolume = monitor.SupportsVolume;
|
|
_showInputSource = monitor.SupportsInputSource;
|
|
_showPowerState = monitor.SupportsPowerState;
|
|
_showColorTemperature = monitor.SupportsColorTemperature;
|
|
|
|
// Initialize basic properties from monitor
|
|
_brightness = monitor.CurrentBrightness;
|
|
_contrast = monitor.CurrentContrast;
|
|
_volume = monitor.CurrentVolume;
|
|
_isAvailable = monitor.IsAvailable;
|
|
}
|
|
|
|
public string Id => _monitor.Id;
|
|
|
|
public string Name => _monitor.Name;
|
|
|
|
/// <summary>
|
|
/// Gets the monitor number from the underlying monitor model (Windows DISPLAY number)
|
|
/// </summary>
|
|
public int MonitorNumber => _monitor.MonitorNumber;
|
|
|
|
/// <summary>
|
|
/// Gets the display name - includes monitor number when multiple monitors exist.
|
|
/// Follows the same logic as Settings UI's MonitorInfo.DisplayName for consistency.
|
|
/// </summary>
|
|
public string DisplayName
|
|
{
|
|
get
|
|
{
|
|
var monitorCount = _mainViewModel?.Monitors?.Count ?? 0;
|
|
|
|
// Show monitor number only when there are multiple monitors and MonitorNumber is valid
|
|
if (monitorCount > 1 && MonitorNumber > 0)
|
|
{
|
|
return $"{Name} {MonitorNumber}";
|
|
}
|
|
|
|
return Name;
|
|
}
|
|
}
|
|
|
|
public string CommunicationMethod => _monitor.CommunicationMethod;
|
|
|
|
public bool IsInternal => _monitor.CommunicationMethod == "WMI";
|
|
|
|
public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
|
|
|
|
public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
|
|
|
|
/// <summary>
|
|
/// Gets the icon glyph based on communication method
|
|
/// WMI monitors (laptop internal displays) use laptop icon, others use external monitor icon
|
|
/// </summary>
|
|
public string MonitorIconGlyph => _monitor.CommunicationMethod?.Contains("WMI", StringComparison.OrdinalIgnoreCase) == true
|
|
? AppConstants.UI.InternalMonitorGlyph // Laptop icon for WMI
|
|
: AppConstants.UI.ExternalMonitorGlyph; // External monitor icon for DDC/CI and others
|
|
|
|
// Monitor property ranges
|
|
public int MinBrightness => _monitor.MinBrightness;
|
|
|
|
public int MaxBrightness => _monitor.MaxBrightness;
|
|
|
|
public int MinContrast => _monitor.MinContrast;
|
|
|
|
public int MaxContrast => _monitor.MaxContrast;
|
|
|
|
public int MinVolume => _monitor.MinVolume;
|
|
|
|
public int MaxVolume => _monitor.MaxVolume;
|
|
|
|
// Advanced control display logic
|
|
public bool HasAdvancedControls => ShowContrast || ShowVolume;
|
|
|
|
public bool ShowContrast
|
|
{
|
|
get => _showContrast;
|
|
set
|
|
{
|
|
if (_showContrast != value)
|
|
{
|
|
_showContrast = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(HasAdvancedControls));
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ShowVolume
|
|
{
|
|
get => _showVolume;
|
|
set
|
|
{
|
|
if (_showVolume != value)
|
|
{
|
|
_showVolume = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(HasAdvancedControls));
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ShowInputSource
|
|
{
|
|
get => _showInputSource;
|
|
set
|
|
{
|
|
if (_showInputSource != value)
|
|
{
|
|
_showInputSource = value;
|
|
OnPropertyChanged();
|
|
OnMoreButtonPropertiesChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to show power state control in the More Button flyout.
|
|
/// </summary>
|
|
public bool ShowPowerState
|
|
{
|
|
get => _showPowerState && SupportsPowerState;
|
|
set
|
|
{
|
|
if (_showPowerState != value)
|
|
{
|
|
_showPowerState = value;
|
|
OnPropertyChanged();
|
|
OnMoreButtonPropertiesChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the More Button should be visible.
|
|
/// Visible when at least one feature (InputSource or PowerState) is enabled.
|
|
/// </summary>
|
|
public bool ShowMoreButton => ShowInputSource || ShowPowerState;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether to show separator after Input Source section.
|
|
/// Only shown when both InputSource and PowerState are visible.
|
|
/// </summary>
|
|
public bool ShowSeparatorAfterInputSource => ShowInputSource && ShowPowerState;
|
|
|
|
/// <summary>
|
|
/// Notifies property changes for More Button related properties.
|
|
/// </summary>
|
|
private void OnMoreButtonPropertiesChanged()
|
|
{
|
|
OnPropertyChanged(nameof(ShowMoreButton));
|
|
OnPropertyChanged(nameof(ShowSeparatorAfterInputSource));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to show rotation controls (controlled by Settings UI, default false).
|
|
/// </summary>
|
|
public bool ShowRotation
|
|
{
|
|
get => _showRotation;
|
|
set
|
|
{
|
|
if (_showRotation != value)
|
|
{
|
|
_showRotation = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current rotation/orientation of the monitor (0=normal, 1=90°, 2=180°, 3=270°)
|
|
/// </summary>
|
|
public int CurrentRotation => _monitor.Orientation;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the current rotation is 0° (normal/default).
|
|
/// </summary>
|
|
public bool IsRotation0 => CurrentRotation == 0;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the current rotation is 90° (rotated right).
|
|
/// </summary>
|
|
public bool IsRotation1 => CurrentRotation == 1;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the current rotation is 180° (inverted).
|
|
/// </summary>
|
|
public bool IsRotation2 => CurrentRotation == 2;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the current rotation is 270° (rotated left).
|
|
/// </summary>
|
|
public bool IsRotation3 => CurrentRotation == 3;
|
|
|
|
/// <summary>
|
|
/// Set rotation/orientation for this monitor.
|
|
/// Note: MonitorManager.SetRotationAsync will refresh all monitors' orientations after success,
|
|
/// which triggers PropertyChanged through OnMonitorPropertyChanged - no manual notification needed here.
|
|
/// </summary>
|
|
/// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
|
|
public async Task SetRotationAsync(int orientation)
|
|
{
|
|
// Validate orientation range (0=normal, 1=90°, 2=180°, 3=270°)
|
|
if (orientation < 0 || orientation > 3)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If already at this orientation, do nothing
|
|
if (CurrentRotation == orientation)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var result = await _monitorManager.SetRotationAsync(Id, orientation);
|
|
|
|
if (!result.IsSuccess)
|
|
{
|
|
Logger.LogWarning($"[{Id}] Failed to set rotation: {result.ErrorMessage}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"[{Id}] Exception setting rotation: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public int Brightness
|
|
{
|
|
get => _brightness;
|
|
set
|
|
{
|
|
if (_brightness != value)
|
|
{
|
|
_ = SetBrightnessAsync(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets color temperature VCP preset value (from VCP code 0x14).
|
|
/// Read-only in flyout UI - controlled via Settings UI.
|
|
/// Returns the raw VCP value (e.g., 0x05 for 6500K).
|
|
/// </summary>
|
|
public int ColorTemperature => _monitor.CurrentColorTemperature;
|
|
|
|
/// <summary>
|
|
/// Gets human-readable color temperature preset name (e.g., "6500K", "sRGB")
|
|
/// </summary>
|
|
public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this monitor supports color temperature via VCP 0x14
|
|
/// </summary>
|
|
public bool SupportsColorTemperature => _monitor.SupportsColorTemperature;
|
|
|
|
private List<ColorTemperatureItem>? _availableColorPresets;
|
|
private bool _showColorTemperature;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to show color temperature switcher (controlled by Settings UI, default false).
|
|
/// </summary>
|
|
public bool ShowColorTemperature
|
|
{
|
|
get => _showColorTemperature && SupportsColorTemperature;
|
|
set
|
|
{
|
|
if (_showColorTemperature != value)
|
|
{
|
|
_showColorTemperature = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets available color temperature presets for this monitor
|
|
/// </summary>
|
|
public List<ColorTemperatureItem>? AvailableColorPresets
|
|
{
|
|
get
|
|
{
|
|
if (_availableColorPresets == null && SupportsColorTemperature)
|
|
{
|
|
RefreshAvailableColorPresets();
|
|
}
|
|
|
|
return _availableColorPresets;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Standard MCCS color temperature presets (VCP 0x14 values) to use as fallback
|
|
/// when the monitor doesn't report discrete values in its capabilities string.
|
|
/// </summary>
|
|
private static readonly int[] StandardColorTemperaturePresets = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08, 0x09, 0x0A, 0x0B };
|
|
|
|
/// <summary>
|
|
/// Refresh the list of available color temperature presets based on monitor capabilities
|
|
/// </summary>
|
|
private void RefreshAvailableColorPresets()
|
|
{
|
|
if (!SupportsColorTemperature)
|
|
{
|
|
_availableColorPresets = null;
|
|
return;
|
|
}
|
|
|
|
IEnumerable<int> presetValues;
|
|
var vcpInfo = VcpCapabilitiesInfo;
|
|
|
|
// Try to get discrete values from capabilities string
|
|
if (vcpInfo != null &&
|
|
vcpInfo.SupportedVcpCodes.TryGetValue(0x14, out var colorTempInfo) &&
|
|
colorTempInfo.HasDiscreteValues &&
|
|
colorTempInfo.SupportedValues.Count > 0)
|
|
{
|
|
// Use values from capabilities string
|
|
presetValues = colorTempInfo.SupportedValues;
|
|
}
|
|
else
|
|
{
|
|
// Fallback to standard MCCS presets when capabilities don't list discrete values
|
|
presetValues = StandardColorTemperaturePresets;
|
|
}
|
|
|
|
_availableColorPresets = presetValues.Select(value => new ColorTemperatureItem
|
|
{
|
|
VcpValue = value,
|
|
DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value),
|
|
IsSelected = value == _monitor.CurrentColorTemperature,
|
|
MonitorId = _monitor.Id,
|
|
}).ToList();
|
|
|
|
OnPropertyChanged(nameof(AvailableColorPresets));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this monitor supports input source switching via VCP 0x60
|
|
/// </summary>
|
|
public bool SupportsInputSource => _monitor.SupportsInputSource;
|
|
|
|
/// <summary>
|
|
/// Gets current input source VCP value (from VCP code 0x60)
|
|
/// </summary>
|
|
public int CurrentInputSource => _monitor.CurrentInputSource;
|
|
|
|
/// <summary>
|
|
/// Gets human-readable current input source name (e.g., "HDMI-1", "DisplayPort-1")
|
|
/// </summary>
|
|
public string CurrentInputSourceName => _monitor.InputSourceName;
|
|
|
|
private List<InputSourceItem>? _availableInputSources;
|
|
|
|
/// <summary>
|
|
/// Gets available input sources for this monitor
|
|
/// </summary>
|
|
public List<InputSourceItem>? AvailableInputSources
|
|
{
|
|
get
|
|
{
|
|
if (_availableInputSources == null && SupportsInputSource)
|
|
{
|
|
RefreshAvailableInputSources();
|
|
}
|
|
|
|
return _availableInputSources;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refresh the list of available input sources based on monitor capabilities
|
|
/// </summary>
|
|
private void RefreshAvailableInputSources()
|
|
{
|
|
var supportedSources = _monitor.SupportedInputSources;
|
|
if (supportedSources == null || supportedSources.Count == 0)
|
|
{
|
|
_availableInputSources = null;
|
|
return;
|
|
}
|
|
|
|
_availableInputSources = supportedSources.Select(value => new InputSourceItem
|
|
{
|
|
Value = value,
|
|
Name = Common.Utils.VcpNames.GetValueName(0x60, value) ?? $"Source 0x{value:X2}",
|
|
SelectionVisibility = value == _monitor.CurrentInputSource ? Visibility.Visible : Visibility.Collapsed,
|
|
MonitorId = _monitor.Id,
|
|
}).ToList();
|
|
|
|
OnPropertyChanged(nameof(AvailableInputSources));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set input source for this monitor
|
|
/// </summary>
|
|
public async Task SetInputSourceAsync(int inputSource)
|
|
{
|
|
try
|
|
{
|
|
var result = await _monitorManager.SetInputSourceAsync(Id, inputSource);
|
|
|
|
if (result.IsSuccess)
|
|
{
|
|
OnPropertyChanged(nameof(CurrentInputSource));
|
|
OnPropertyChanged(nameof(CurrentInputSourceName));
|
|
RefreshAvailableInputSources();
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning($"[{Id}] Failed to set input source: {result.ErrorMessage}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"[{Id}] Exception setting input source: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Command to set input source
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private async Task SetInputSource(int? source)
|
|
{
|
|
if (source.HasValue)
|
|
{
|
|
await SetInputSourceAsync(source.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this monitor supports power state control via VCP 0xD6
|
|
/// </summary>
|
|
public bool SupportsPowerState => _monitor.SupportsPowerState;
|
|
|
|
private List<PowerStateItem>? _availablePowerStates;
|
|
|
|
/// <summary>
|
|
/// Gets available power states for this monitor.
|
|
/// The current power state is shown as selected based on the monitor's actual state.
|
|
/// </summary>
|
|
public List<PowerStateItem>? AvailablePowerStates
|
|
{
|
|
get
|
|
{
|
|
if (_availablePowerStates == null && SupportsPowerState)
|
|
{
|
|
RefreshAvailablePowerStates();
|
|
}
|
|
|
|
return _availablePowerStates;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refresh the list of available power states based on monitor capabilities
|
|
/// </summary>
|
|
private void RefreshAvailablePowerStates()
|
|
{
|
|
var supportedStates = _monitor.SupportedPowerStates;
|
|
if (supportedStates == null || supportedStates.Count == 0)
|
|
{
|
|
_availablePowerStates = null;
|
|
return;
|
|
}
|
|
|
|
_availablePowerStates = supportedStates.Select(value => new PowerStateItem
|
|
{
|
|
Value = value,
|
|
Name = Common.Utils.VcpNames.GetValueName(0xD6, value) ?? $"State 0x{value:X2}",
|
|
IsSelected = value == _monitor.CurrentPowerState,
|
|
MonitorId = _monitor.Id,
|
|
}).ToList();
|
|
|
|
OnPropertyChanged(nameof(AvailablePowerStates));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set power state for this monitor.
|
|
/// Note: Setting any state other than "On" will turn off the display.
|
|
/// </summary>
|
|
public async Task SetPowerStateAsync(int powerState)
|
|
{
|
|
try
|
|
{
|
|
var result = await _monitorManager.SetPowerStateAsync(Id, powerState);
|
|
|
|
if (result.IsSuccess)
|
|
{
|
|
// Update the model's power state and refresh UI
|
|
_monitor.CurrentPowerState = powerState;
|
|
RefreshAvailablePowerStates();
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning($"[{Id}] Failed to set power state: {result.ErrorMessage}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"[{Id}] Exception setting power state: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Command to set power state
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private async Task SetPowerState(int? state)
|
|
{
|
|
if (state.HasValue)
|
|
{
|
|
await SetPowerStateAsync(state.Value);
|
|
}
|
|
}
|
|
|
|
public int Contrast
|
|
{
|
|
get => _contrast;
|
|
set
|
|
{
|
|
if (_contrast != value)
|
|
{
|
|
_ = SetContrastAsync(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public int Volume
|
|
{
|
|
get => _volume;
|
|
set
|
|
{
|
|
if (_volume != value)
|
|
{
|
|
_ = SetVolumeAsync(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsAvailable
|
|
{
|
|
get => _isAvailable;
|
|
set
|
|
{
|
|
_isAvailable = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SetBrightness(int? brightness)
|
|
{
|
|
if (brightness.HasValue)
|
|
{
|
|
Brightness = brightness.Value;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SetContrast(int? contrast)
|
|
{
|
|
if (contrast.HasValue)
|
|
{
|
|
Contrast = contrast.Value;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SetVolume(int? volume)
|
|
{
|
|
if (volume.HasValue)
|
|
{
|
|
Volume = volume.Value;
|
|
}
|
|
}
|
|
|
|
public int ContrastPercent
|
|
{
|
|
get => MapToPercent(_contrast, MinContrast, MaxContrast);
|
|
set
|
|
{
|
|
var actualValue = MapFromPercent(value, MinContrast, MaxContrast);
|
|
Contrast = actualValue;
|
|
}
|
|
}
|
|
|
|
// Mapping functions for percentage conversion
|
|
private int MapToPercent(int value, int min, int max)
|
|
{
|
|
if (max <= min)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return (int)Math.Round((value - min) * 100.0 / (max - min));
|
|
}
|
|
|
|
private int MapFromPercent(int percent, int min, int max)
|
|
{
|
|
if (max <= min)
|
|
{
|
|
return min;
|
|
}
|
|
|
|
percent = Math.Clamp(percent, 0, 100);
|
|
return min + (int)Math.Round(percent * (max - min) / 100.0);
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(MainViewModel.IsInteractionEnabled))
|
|
{
|
|
OnPropertyChanged(nameof(IsInteractionEnabled));
|
|
}
|
|
else if (e.PropertyName == nameof(MainViewModel.HasMonitors))
|
|
{
|
|
// Monitor count changed, update display name to show/hide number suffix
|
|
OnPropertyChanged(nameof(DisplayName));
|
|
}
|
|
}
|
|
|
|
private void OnMonitorPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
// Forward Orientation changes from underlying Monitor to ViewModel properties
|
|
// This is important for mirror mode where MonitorManager.RefreshAllOrientations()
|
|
// updates multiple monitors sharing the same GdiDeviceName
|
|
if (e.PropertyName == nameof(Monitor.Orientation))
|
|
{
|
|
OnPropertyChanged(nameof(CurrentRotation));
|
|
OnPropertyChanged(nameof(IsRotation0));
|
|
OnPropertyChanged(nameof(IsRotation1));
|
|
OnPropertyChanged(nameof(IsRotation2));
|
|
OnPropertyChanged(nameof(IsRotation3));
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Unsubscribe from MainViewModel events
|
|
if (_mainViewModel != null)
|
|
{
|
|
_mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
|
|
}
|
|
|
|
// Unsubscribe from underlying Monitor events
|
|
_monitor.PropertyChanged -= OnMonitorPropertyChanged;
|
|
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|