Files
PowerToys/src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml.cs
moooyo 0d41d45a64 [Settings] Decouple Settings.UI.Library from PowerDisplay.Lib to fix … (#46325)
<!-- 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
Fixes PowerToys Run crash (`FileNotFoundException` for
`PowerDisplay.Lib.dll`) caused by `Settings.UI.Library` having a
transitive dependency on `PowerDisplay.Lib`.

`SettingsSerializationContext` registered PowerDisplay profile types
(`PowerDisplayProfile`, `PowerDisplayProfiles`, `ProfileMonitorSetting`)
via `[JsonSerializable]` attributes, which forced the CLR to load
`PowerDisplay.Lib.dll` at startup. PowerToys Run depends on
`Settings.UI.Library` but does not ship `PowerDisplay.Lib.dll`, causing
the crash.



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

- [x] Closes: #46261
<!-- - [ ] 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 (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 07:06:07 +00:00

423 lines
15 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using CustomVcpValueMapping = Microsoft.PowerToys.Settings.UI.Library.CustomVcpValueMapping;
namespace Microsoft.PowerToys.Settings.UI.Views
{
/// <summary>
/// Dialog for creating/editing custom VCP value name mappings
/// </summary>
public sealed partial class CustomVcpMappingEditorDialog : ContentDialog, INotifyPropertyChanged
{
/// <summary>
/// Special value to indicate "Custom value" option in the ComboBox
/// </summary>
private const int CustomValueMarker = -1;
/// <summary>
/// Represents a selectable VCP value item in the Value ComboBox
/// </summary>
public class VcpValueItem
{
public int Value { get; set; }
public string DisplayName { get; set; } = string.Empty;
public bool IsCustomOption => Value == CustomValueMarker;
}
/// <summary>
/// Represents a selectable monitor item in the Monitor ComboBox
/// </summary>
public class MonitorItem
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}
private readonly IEnumerable<MonitorInfo>? _monitors;
private ObservableCollection<VcpValueItem> _availableValues = new();
private ObservableCollection<MonitorItem> _availableMonitors = new();
private byte _selectedVcpCode;
private int _selectedValue;
private string _customName = string.Empty;
private bool _canSave;
private bool _showCustomValueInput;
private bool _showMonitorSelector;
private int _customValueParsed;
private bool _applyToAll = true;
private string _selectedMonitorId = string.Empty;
private string _selectedMonitorName = string.Empty;
public CustomVcpMappingEditorDialog(IEnumerable<MonitorInfo>? monitors)
{
_monitors = monitors;
this.InitializeComponent();
// Set localized strings for ContentDialog
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
Title = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_Title");
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Save");
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel");
// Set VCP code ComboBox items content dynamically using localized names
VcpCodeItem_0x14.Content = GetFormattedVcpCodeName(resourceLoader, 0x14);
VcpCodeItem_0x60.Content = GetFormattedVcpCodeName(resourceLoader, 0x60);
// Populate monitor list
PopulateMonitorList();
// Default to Color Temperature (0x14)
VcpCodeComboBox.SelectedIndex = 0;
}
/// <summary>
/// Gets the result mapping after dialog closes with Primary button
/// </summary>
public CustomVcpValueMapping? ResultMapping { get; private set; }
/// <summary>
/// Gets the available values for the selected VCP code
/// </summary>
public ObservableCollection<VcpValueItem> AvailableValues
{
get => _availableValues;
private set
{
_availableValues = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets the available monitors for selection
/// </summary>
public ObservableCollection<MonitorItem> AvailableMonitors
{
get => _availableMonitors;
private set
{
_availableMonitors = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets a value indicating whether the dialog can be saved
/// </summary>
public bool CanSave
{
get => _canSave;
private set
{
if (_canSave != value)
{
_canSave = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets a value indicating whether to show the custom value input TextBox
/// </summary>
public Visibility ShowCustomValueInput => _showCustomValueInput ? Visibility.Visible : Visibility.Collapsed;
/// <summary>
/// Gets a value indicating whether to show the monitor selector ComboBox
/// </summary>
public Visibility ShowMonitorSelector => _showMonitorSelector ? Visibility.Visible : Visibility.Collapsed;
private void SetShowCustomValueInput(bool value)
{
if (_showCustomValueInput != value)
{
_showCustomValueInput = value;
OnPropertyChanged(nameof(ShowCustomValueInput));
}
}
private void SetShowMonitorSelector(bool value)
{
if (_showMonitorSelector != value)
{
_showMonitorSelector = value;
OnPropertyChanged(nameof(ShowMonitorSelector));
}
}
private void PopulateMonitorList()
{
AvailableMonitors = new ObservableCollection<MonitorItem>(
_monitors?.Select(m => new MonitorItem { Id = m.Id, DisplayName = m.DisplayName })
?? Enumerable.Empty<MonitorItem>());
if (AvailableMonitors.Count > 0)
{
MonitorComboBox.SelectedIndex = 0;
}
}
/// <summary>
/// Pre-fill the dialog with existing mapping data for editing
/// </summary>
public void PreFillMapping(CustomVcpValueMapping mapping)
{
if (mapping is null)
{
return;
}
// Select the VCP code
VcpCodeComboBox.SelectedIndex = mapping.VcpCode == 0x14 ? 0 : 1;
// Populate values for the selected VCP code
PopulateValuesForVcpCode(mapping.VcpCode);
// Try to select the value in the ComboBox
var matchingItem = AvailableValues.FirstOrDefault(v => !v.IsCustomOption && v.Value == mapping.Value);
if (matchingItem is not null)
{
ValueComboBox.SelectedItem = matchingItem;
}
else
{
// Value not found in list, select "Custom value" option and fill the TextBox
ValueComboBox.SelectedItem = AvailableValues.FirstOrDefault(v => v.IsCustomOption);
CustomValueTextBox.Text = $"0x{mapping.Value:X2}";
_customValueParsed = mapping.Value;
}
// Set the custom name
CustomNameTextBox.Text = mapping.CustomName;
_customName = mapping.CustomName;
// Set apply scope
_applyToAll = mapping.ApplyToAll;
ApplyToAllToggle.IsOn = mapping.ApplyToAll;
SetShowMonitorSelector(!mapping.ApplyToAll);
// Select the target monitor if not applying to all
if (!mapping.ApplyToAll && !string.IsNullOrEmpty(mapping.TargetMonitorId))
{
var targetMonitor = AvailableMonitors.FirstOrDefault(m => m.Id == mapping.TargetMonitorId);
if (targetMonitor is not null)
{
MonitorComboBox.SelectedItem = targetMonitor;
_selectedMonitorId = targetMonitor.Id;
_selectedMonitorName = targetMonitor.DisplayName;
}
}
UpdateCanSave();
}
private void VcpCodeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (VcpCodeComboBox.SelectedItem is ComboBoxItem selectedItem &&
selectedItem.Tag is string tagValue &&
byte.TryParse(tagValue, out byte vcpCode))
{
_selectedVcpCode = vcpCode;
PopulateValuesForVcpCode(vcpCode);
UpdateCanSave();
}
}
private void PopulateValuesForVcpCode(byte vcpCode)
{
var values = new ObservableCollection<VcpValueItem>();
var seenValues = new HashSet<int>();
// Collect values from all monitors
if (_monitors is not null)
{
foreach (var monitor in _monitors)
{
if (monitor.VcpCodesFormatted is null)
{
continue;
}
// Find the VCP code entry
var vcpEntry = monitor.VcpCodesFormatted.FirstOrDefault(v =>
!string.IsNullOrEmpty(v.Code) &&
TryParseHexCode(v.Code, out int code) &&
code == vcpCode);
if (vcpEntry?.ValueList is null)
{
continue;
}
// Add each value from this monitor
foreach (var valueInfo in vcpEntry.ValueList)
{
if (TryParseHexCode(valueInfo.Value, out int vcpValue) && !seenValues.Contains(vcpValue))
{
seenValues.Add(vcpValue);
var displayName = !string.IsNullOrEmpty(valueInfo.Name)
? $"{valueInfo.Name} (0x{vcpValue:X2})"
: VcpNames.GetFormattedValueName(vcpCode, vcpValue);
values.Add(new VcpValueItem
{
Value = vcpValue,
DisplayName = displayName,
});
}
}
}
}
// If no values found from monitors, fall back to built-in values from VcpNames
if (values.Count == 0)
{
var builtInValues = VcpNames.GetValueMappings(vcpCode);
if (builtInValues is not null)
{
foreach (var kvp in builtInValues)
{
values.Add(new VcpValueItem
{
Value = kvp.Key,
DisplayName = $"{kvp.Value} (0x{kvp.Key:X2})",
});
}
}
}
// Sort by value
var sortedValues = new ObservableCollection<VcpValueItem>(values.OrderBy(v => v.Value));
// Add "Custom value" option at the end
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
sortedValues.Add(new VcpValueItem
{
Value = CustomValueMarker,
DisplayName = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_CustomValueOption"),
});
AvailableValues = sortedValues;
// Select first item if available
if (sortedValues.Count > 0)
{
ValueComboBox.SelectedIndex = 0;
}
}
private static bool TryParseHexCode(string? hex, out int result)
{
result = 0;
if (string.IsNullOrEmpty(hex))
{
return false;
}
var cleanHex = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex;
return int.TryParse(cleanHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result);
}
private static string GetFormattedVcpCodeName(Windows.ApplicationModel.Resources.ResourceLoader resourceLoader, byte vcpCode)
{
var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}";
var localizedName = resourceLoader.GetString(resourceKey);
var name = string.IsNullOrEmpty(localizedName) ? VcpNames.GetCodeName(vcpCode) : localizedName;
return $"{name} (0x{vcpCode:X2})";
}
private void ValueComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ValueComboBox.SelectedItem is VcpValueItem selectedItem)
{
SetShowCustomValueInput(selectedItem.IsCustomOption);
_selectedValue = selectedItem.IsCustomOption ? 0 : selectedItem.Value;
UpdateCanSave();
}
}
private void CustomValueTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
_customValueParsed = TryParseHexCode(CustomValueTextBox.Text?.Trim(), out int parsed) ? parsed : 0;
UpdateCanSave();
}
private void CustomNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
_customName = CustomNameTextBox.Text?.Trim() ?? string.Empty;
UpdateCanSave();
}
private void ApplyToAllToggle_Toggled(object sender, RoutedEventArgs e)
{
_applyToAll = ApplyToAllToggle.IsOn;
SetShowMonitorSelector(!_applyToAll);
UpdateCanSave();
}
private void MonitorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (MonitorComboBox.SelectedItem is MonitorItem selectedMonitor)
{
_selectedMonitorId = selectedMonitor.Id;
_selectedMonitorName = selectedMonitor.DisplayName;
UpdateCanSave();
}
}
private void UpdateCanSave()
{
var hasValidValue = _showCustomValueInput
? _customValueParsed > 0
: ValueComboBox.SelectedItem is VcpValueItem item && !item.IsCustomOption;
CanSave = _selectedVcpCode > 0 &&
hasValidValue &&
!string.IsNullOrWhiteSpace(_customName) &&
(_applyToAll || !string.IsNullOrEmpty(_selectedMonitorId));
}
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (CanSave)
{
int finalValue = _showCustomValueInput ? _customValueParsed : _selectedValue;
ResultMapping = new CustomVcpValueMapping
{
VcpCode = _selectedVcpCode,
Value = finalValue,
CustomName = _customName,
ApplyToAll = _applyToAll,
TargetMonitorId = _applyToAll ? string.Empty : _selectedMonitorId,
TargetMonitorName = _applyToAll ? string.Empty : _selectedMonitorName,
};
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}