Files
PowerToys/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs
moooyo 3336c134dd [PowerDisplay] Add custom vcp code name map and fix some bugs (#45355)
<!-- 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
1. Fix quick access not working bug
2. Add custom value mapping
3. Fix some vcp slider visibility bug

demo for custom vcp value name mapping:
<img width="1399" height="744" alt="image"
src="https://github.com/user-attachments/assets/517e4dbb-409a-4e43-b15a-d0d31e59ce49"
/>
<img width="1379" height="337" alt="image"
src="https://github.com/user-attachments/assets/18f6f389-089c-4441-ad9f-5c45cac53814"
/>
<img width="521" height="1152" alt="image"
src="https://github.com/user-attachments/assets/27b5f796-66fa-4781-b16f-4770bebf3504"
/>
<img width="295" height="808" alt="image"
src="https://github.com/user-attachments/assets/54eaf5b9-5d54-4531-a40b-de3113122715"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **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>
2026-02-05 17:02:55 +08:00

902 lines
28 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;
/// <summary>
/// Gets a value indicating whether this monitor supports contrast control via VCP 0x12
/// </summary>
public bool SupportsContrast => _monitor.SupportsContrast;
/// <summary>
/// Gets a value indicating whether this monitor supports volume control via VCP 0x62
/// </summary>
public bool SupportsVolume => _monitor.SupportsVolume;
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")
/// Uses custom mappings if available; falls back to built-in names if not.
/// </summary>
public string ColorTemperaturePresetName =>
Common.Utils.VcpNames.GetFormattedValueName(0x14, _monitor.CurrentColorTemperature, _mainViewModel?.CustomVcpMappings, _monitor.Id);
/// <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, _mainViewModel?.CustomVcpMappings, _monitor.Id),
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")
/// Uses custom mappings if available; falls back to built-in names if not.
/// </summary>
public string CurrentInputSourceName =>
Common.Utils.VcpNames.GetValueName(0x60, _monitor.CurrentInputSource, _mainViewModel?.CustomVcpMappings, _monitor.Id)
?? $"Source 0x{_monitor.CurrentInputSource:X2}";
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, _mainViewModel?.CustomVcpMappings, _monitor.Id) ?? $"Source 0x{value:X2}",
SelectionVisibility = value == _monitor.CurrentInputSource ? Visibility.Visible : Visibility.Collapsed,
MonitorId = _monitor.Id,
}).ToList();
OnPropertyChanged(nameof(AvailableInputSources));
}
/// <summary>
/// Refresh custom VCP name displays after settings change.
/// Called when CustomVcpMappings is updated from Settings UI.
/// </summary>
public void RefreshCustomVcpNames()
{
// Refresh color temperature names
OnPropertyChanged(nameof(ColorTemperaturePresetName));
_availableColorPresets = null; // Force rebuild with new custom names
OnPropertyChanged(nameof(AvailableColorPresets));
// Refresh input source names
OnPropertyChanged(nameof(CurrentInputSourceName));
_availableInputSources = null; // Force rebuild with new custom names
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);
}
}