Files
PowerToys/src/settings-ui/Settings.UI.Library/MonitorInfo.cs
moooyo 18efa0559c Introduce new utility PowerDisplay to control your monitor settings (#42642)
<!-- 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>
2026-02-03 13:53:25 +08:00

695 lines
23 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.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MonitorInfo : Observable
{
private string _name = string.Empty;
private string _id = string.Empty;
private string _communicationMethod = string.Empty;
private int _currentBrightness;
private int _colorTemperatureVcp = 0x05; // Default to 6500K preset (VCP 0x14 value)
private int _contrast;
private int _volume;
private bool _isHidden;
private bool _enableContrast;
private bool _enableVolume;
private bool _enableInputSource;
private bool _enableRotation;
private bool _enableColorTemperature;
private bool _enablePowerState;
private string _capabilitiesRaw = string.Empty;
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
private int _monitorNumber;
private int _totalMonitorCount;
// Feature support status (determined from capabilities)
private bool _supportsBrightness = true; // Brightness always shown even if unsupported
private bool _supportsContrast;
private bool _supportsColorTemperature;
private bool _supportsVolume;
private bool _supportsInputSource;
private bool _supportsPowerState;
// Cached color temperature presets (computed from VcpCodesFormatted)
private ObservableCollection<ColorPresetItem> _availableColorPresetsCache;
private ObservableCollection<ColorPresetItem> _colorPresetsForDisplayCache;
private int _lastColorTemperatureVcpForCache = -1;
/// <summary>
/// Invalidates the color preset cache and notifies property changes.
/// Call this when VcpCodesFormatted or SupportsColorTemperature changes.
/// </summary>
private void InvalidateColorPresetCache()
{
_availableColorPresetsCache = null;
_colorPresetsForDisplayCache = null;
_lastColorTemperatureVcpForCache = -1;
OnPropertyChanged(nameof(ColorPresetsForDisplay));
}
public MonitorInfo()
{
}
[JsonPropertyName("name")]
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayName));
}
}
}
/// <summary>
/// Gets or sets the monitor number (Windows DISPLAY number, e.g., 1, 2, 3...).
/// </summary>
[JsonPropertyName("monitorNumber")]
public int MonitorNumber
{
get => _monitorNumber;
set
{
if (_monitorNumber != value)
{
_monitorNumber = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayName));
}
}
}
/// <summary>
/// Gets or sets the total number of monitors (used for dynamic display name).
/// This is not serialized; it's set by the ViewModel.
/// </summary>
[JsonIgnore]
public int TotalMonitorCount
{
get => _totalMonitorCount;
set
{
if (_totalMonitorCount != value)
{
_totalMonitorCount = value;
OnPropertyChanged(nameof(DisplayName));
}
}
}
/// <summary>
/// Gets the display name - includes monitor number when multiple monitors exist.
/// Follows the same logic as PowerDisplay UI's MonitorViewModel.DisplayName.
/// </summary>
[JsonIgnore]
public string DisplayName
{
get
{
// Show monitor number only when there are multiple monitors and MonitorNumber is valid
if (TotalMonitorCount > 1 && MonitorNumber > 0)
{
return $"{Name} {MonitorNumber}";
}
return Name;
}
}
public string MonitorIconGlyph => CommunicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase)
? "\uE7F8" // Laptop icon for WMI
: "\uE7F4"; // External monitor icon for DDC/CI and others
[JsonPropertyName("id")]
public string Id
{
get => _id;
set
{
if (_id != value)
{
_id = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("communicationMethod")]
public string CommunicationMethod
{
get => _communicationMethod;
set
{
if (_communicationMethod != value)
{
_communicationMethod = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("currentBrightness")]
public int CurrentBrightness
{
get => _currentBrightness;
set
{
if (_currentBrightness != value)
{
_currentBrightness = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
/// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
/// </summary>
[JsonPropertyName("colorTemperatureVcp")]
public int ColorTemperatureVcp
{
get => _colorTemperatureVcp;
set
{
if (_colorTemperatureVcp != value)
{
_colorTemperatureVcp = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Update display list when current value changes
}
}
}
/// <summary>
/// Gets or sets the current contrast value (0-100).
/// </summary>
[JsonPropertyName("contrast")]
public int Contrast
{
get => _contrast;
set
{
if (_contrast != value)
{
_contrast = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets the current volume value (0-100).
/// </summary>
[JsonPropertyName("volume")]
public int Volume
{
get => _volume;
set
{
if (_volume != value)
{
_volume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("isHidden")]
public bool IsHidden
{
get => _isHidden;
set
{
if (_isHidden != value)
{
_isHidden = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableContrast")]
public bool EnableContrast
{
get => _enableContrast;
set
{
if (_enableContrast != value)
{
_enableContrast = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableVolume")]
public bool EnableVolume
{
get => _enableVolume;
set
{
if (_enableVolume != value)
{
_enableVolume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableInputSource")]
public bool EnableInputSource
{
get => _enableInputSource;
set
{
if (_enableInputSource != value)
{
_enableInputSource = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableRotation")]
public bool EnableRotation
{
get => _enableRotation;
set
{
if (_enableRotation != value)
{
_enableRotation = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableColorTemperature")]
public bool EnableColorTemperature
{
get => _enableColorTemperature;
set
{
if (_enableColorTemperature != value)
{
_enableColorTemperature = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enablePowerState")]
public bool EnablePowerState
{
get => _enablePowerState;
set
{
if (_enablePowerState != value)
{
_enablePowerState = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("capabilitiesRaw")]
public string CapabilitiesRaw
{
get => _capabilitiesRaw;
set
{
if (_capabilitiesRaw != value)
{
_capabilitiesRaw = value ?? string.Empty;
OnPropertyChanged();
OnPropertyChanged(nameof(HasCapabilities));
}
}
}
[JsonPropertyName("vcpCodesFormatted")]
public List<VcpCodeDisplayInfo> VcpCodesFormatted
{
get => _vcpCodesFormatted;
set
{
var newValue = value ?? new List<VcpCodeDisplayInfo>();
// Only update if content actually changed (compare by VCP code list content)
if (AreVcpCodesEqual(_vcpCodesFormatted, newValue))
{
return;
}
_vcpCodesFormatted = newValue;
OnPropertyChanged();
InvalidateColorPresetCache();
}
}
/// <summary>
/// Compare two VcpCodesFormatted lists for equality by content.
/// Returns true if both lists have the same VCP codes (by code value).
/// </summary>
private static bool AreVcpCodesEqual(List<VcpCodeDisplayInfo> list1, List<VcpCodeDisplayInfo> list2)
{
if (list1 == null && list2 == null)
{
return true;
}
if (list1 == null || list2 == null)
{
return false;
}
if (list1.Count != list2.Count)
{
return false;
}
// Compare by code values - order matters for our use case
for (int i = 0; i < list1.Count; i++)
{
if (list1[i].Code != list2[i].Code)
{
return false;
}
// Also compare ValueList count to detect preset changes
var values1 = list1[i].ValueList;
var values2 = list2[i].ValueList;
if ((values1?.Count ?? 0) != (values2?.Count ?? 0))
{
return false;
}
}
return true;
}
[JsonPropertyName("supportsBrightness")]
public bool SupportsBrightness
{
get => _supportsBrightness;
set
{
if (_supportsBrightness != value)
{
_supportsBrightness = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsContrast")]
public bool SupportsContrast
{
get => _supportsContrast;
set
{
if (_supportsContrast != value)
{
_supportsContrast = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsColorTemperature")]
public bool SupportsColorTemperature
{
get => _supportsColorTemperature;
set
{
if (_supportsColorTemperature != value)
{
_supportsColorTemperature = value;
OnPropertyChanged();
InvalidateColorPresetCache(); // Notifies ColorPresetsForDisplay
}
}
}
[JsonPropertyName("supportsVolume")]
public bool SupportsVolume
{
get => _supportsVolume;
set
{
if (_supportsVolume != value)
{
_supportsVolume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsInputSource")]
public bool SupportsInputSource
{
get => _supportsInputSource;
set
{
if (_supportsInputSource != value)
{
_supportsInputSource = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsPowerState")]
public bool SupportsPowerState
{
get => _supportsPowerState;
set
{
if (_supportsPowerState != value)
{
_supportsPowerState = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets available color temperature presets computed from VcpCodesFormatted (VCP code 0x14).
/// This is a computed property that parses the VCP capabilities data on-demand.
/// </summary>
private ObservableCollection<ColorPresetItem> AvailableColorPresets
{
get
{
// Return cached value if available
if (_availableColorPresetsCache != null)
{
return _availableColorPresetsCache;
}
// Compute from VcpCodesFormatted
_availableColorPresetsCache = ComputeAvailableColorPresets();
return _availableColorPresetsCache;
}
}
/// <summary>
/// Compute available color presets from VcpCodesFormatted (VCP code 0x14).
/// Uses ColorTemperatureHelper from PowerDisplay.Lib for shared computation logic.
/// </summary>
private ObservableCollection<ColorPresetItem> ComputeAvailableColorPresets()
{
// Check if color temperature is supported
if (!_supportsColorTemperature || _vcpCodesFormatted == null)
{
return new ObservableCollection<ColorPresetItem>();
}
// Find VCP code 0x14 (Color Temperature / Select Color Preset)
var colorTempVcp = _vcpCodesFormatted.FirstOrDefault(v =>
!string.IsNullOrEmpty(v.Code) &&
int.TryParse(
v.Code.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? v.Code[2..] : v.Code,
System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture,
out int code) &&
code == NativeConstants.VcpCodeSelectColorPreset);
// No VCP 0x14 or no values
if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
{
return new ObservableCollection<ColorPresetItem>();
}
// Extract VCP values as tuples for ColorTemperatureHelper
var colorTempValues = colorTempVcp.ValueList
.Select(valueInfo =>
{
var hex = valueInfo.Value;
if (string.IsNullOrEmpty(hex))
{
return (VcpValue: 0, Name: valueInfo.Name);
}
var cleanHex = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex;
bool parsed = int.TryParse(cleanHex, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int vcpValue);
return (VcpValue: parsed ? vcpValue : 0, Name: valueInfo.Name);
})
.Where(x => x.VcpValue > 0);
// Use shared helper to compute presets, then convert to nested type for XAML compatibility
var basePresets = ColorTemperatureHelper.ComputeColorPresets(colorTempValues);
var presetList = basePresets.Select(p => new ColorPresetItem(p.VcpValue, p.DisplayName));
return new ObservableCollection<ColorPresetItem>(presetList);
}
/// <summary>
/// Gets color presets for display in ComboBox, includes current value if not in preset list.
/// Uses caching to avoid recreating collections on every access.
/// </summary>
[JsonIgnore]
public ObservableCollection<ColorPresetItem> ColorPresetsForDisplay
{
get
{
// Return cached value if available and color temperature hasn't changed
if (_colorPresetsForDisplayCache != null && _lastColorTemperatureVcpForCache == _colorTemperatureVcp)
{
return _colorPresetsForDisplayCache;
}
var presets = AvailableColorPresets;
if (presets == null || presets.Count == 0)
{
_colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>();
_lastColorTemperatureVcpForCache = _colorTemperatureVcp;
return _colorPresetsForDisplayCache;
}
// Check if current value is in the preset list
var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperatureVcp);
if (currentValueInList)
{
// Current value is in the list, return as-is
_colorPresetsForDisplayCache = presets;
}
else
{
// Current value is not in the preset list - add it at the beginning
var displayList = new List<ColorPresetItem>();
// Add current value with "Custom" indicator using shared helper
var displayName = ColorTemperatureHelper.FormatCustomColorTemperatureDisplayName(_colorTemperatureVcp);
displayList.Add(new ColorPresetItem(_colorTemperatureVcp, displayName));
// Add all supported presets
displayList.AddRange(presets);
_colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>(displayList);
}
_lastColorTemperatureVcpForCache = _colorTemperatureVcp;
return _colorPresetsForDisplayCache;
}
}
[JsonIgnore]
public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
[JsonIgnore]
public bool ShowCapabilitiesWarning => _communicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Generate formatted text of all VCP codes for clipboard
/// </summary>
public string GetVcpCodesAsText()
{
if (_vcpCodesFormatted == null || _vcpCodesFormatted.Count == 0)
{
return "No VCP codes detected";
}
var lines = new List<string>();
lines.Add($"VCP Capabilities for: {_name}");
lines.Add($"Monitor ID: {_id}");
lines.Add(string.Empty);
lines.Add("Detected VCP Codes:");
lines.Add(new string('-', 50));
foreach (var vcp in _vcpCodesFormatted)
{
lines.Add(string.Empty);
lines.Add(vcp.Title);
if (vcp.HasValues)
{
lines.Add($" {vcp.Values}");
}
}
lines.Add(string.Empty);
lines.Add(new string('-', 50));
lines.Add($"Total: {_vcpCodesFormatted.Count} VCP codes");
return string.Join(System.Environment.NewLine, lines);
}
/// <summary>
/// Update this monitor's properties from another MonitorInfo instance.
/// This preserves the object reference while updating all properties.
/// </summary>
/// <param name="other">The source MonitorInfo to copy properties from</param>
public void UpdateFrom(MonitorInfo other)
{
if (other == null)
{
return;
}
// Update all properties that can change
Name = other.Name;
Id = other.Id;
CommunicationMethod = other.CommunicationMethod;
CurrentBrightness = other.CurrentBrightness;
Contrast = other.Contrast;
Volume = other.Volume;
ColorTemperatureVcp = other.ColorTemperatureVcp;
IsHidden = other.IsHidden;
EnableContrast = other.EnableContrast;
EnableVolume = other.EnableVolume;
EnableInputSource = other.EnableInputSource;
EnableRotation = other.EnableRotation;
EnableColorTemperature = other.EnableColorTemperature;
EnablePowerState = other.EnablePowerState;
CapabilitiesRaw = other.CapabilitiesRaw;
VcpCodesFormatted = other.VcpCodesFormatted;
SupportsBrightness = other.SupportsBrightness;
SupportsContrast = other.SupportsContrast;
SupportsColorTemperature = other.SupportsColorTemperature;
SupportsVolume = other.SupportsVolume;
SupportsInputSource = other.SupportsInputSource;
SupportsPowerState = other.SupportsPowerState;
MonitorNumber = other.MonitorNumber;
}
}
}