mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
[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>
This commit is contained in:
@@ -119,6 +119,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
eventHandle.Set();
|
||||
}
|
||||
|
||||
return true;
|
||||
case ModuleType.PowerDisplay:
|
||||
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.TogglePowerDisplayEvent()))
|
||||
{
|
||||
eventHandle.Set();
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
ShowSystemTrayIcon = true;
|
||||
ShowProfileSwitcher = true;
|
||||
ShowIdentifyMonitorsButton = true;
|
||||
CustomVcpMappings = new List<CustomVcpValueMapping>();
|
||||
|
||||
// Note: saved_monitor_settings has been moved to monitor_state.json
|
||||
// which is managed separately by PowerDisplay app
|
||||
@@ -61,5 +62,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
/// </summary>
|
||||
[JsonPropertyName("show_identify_monitors_button")]
|
||||
public bool ShowIdentifyMonitorsButton { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets custom VCP value name mappings shared across all monitors.
|
||||
/// Allows users to define custom names for color temperature presets and input sources.
|
||||
/// </summary>
|
||||
[JsonPropertyName("custom_vcp_mappings")]
|
||||
public List<CustomVcpValueMapping> CustomVcpMappings { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<ContentDialog
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.Views.CustomVcpMappingEditorDialog"
|
||||
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"
|
||||
Width="400"
|
||||
MinWidth="400"
|
||||
DefaultButton="Primary"
|
||||
IsPrimaryButtonEnabled="{x:Bind CanSave, Mode=OneWay}"
|
||||
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
|
||||
Style="{StaticResource DefaultContentDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel MinWidth="350" Spacing="16">
|
||||
<!-- VCP Code Selection -->
|
||||
<ComboBox
|
||||
x:Name="VcpCodeComboBox"
|
||||
x:Uid="PowerDisplay_CustomMappingEditor_VcpCode"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="VcpCodeComboBox_SelectionChanged">
|
||||
<ComboBoxItem x:Name="VcpCodeItem_0x14" Tag="20" />
|
||||
<ComboBoxItem x:Name="VcpCodeItem_0x60" Tag="96" />
|
||||
</ComboBox>
|
||||
|
||||
<!-- Value Selection from monitors -->
|
||||
<StackPanel Spacing="8">
|
||||
<ComboBox
|
||||
x:Name="ValueComboBox"
|
||||
x:Uid="PowerDisplay_CustomMappingEditor_ValueComboBox"
|
||||
HorizontalAlignment="Stretch"
|
||||
DisplayMemberPath="DisplayName"
|
||||
ItemsSource="{x:Bind AvailableValues, Mode=OneWay}"
|
||||
SelectedValuePath="Value"
|
||||
SelectionChanged="ValueComboBox_SelectionChanged" />
|
||||
|
||||
<!-- Custom Value Input (shown when "Custom value" is selected) -->
|
||||
<TextBox
|
||||
x:Name="CustomValueTextBox"
|
||||
x:Uid="PowerDisplay_CustomMappingEditor_CustomValueInput"
|
||||
HorizontalAlignment="Stretch"
|
||||
PlaceholderText="0x11"
|
||||
TextChanged="CustomValueTextBox_TextChanged"
|
||||
Visibility="{x:Bind ShowCustomValueInput, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Custom Name Input -->
|
||||
<TextBox
|
||||
x:Name="CustomNameTextBox"
|
||||
x:Uid="PowerDisplay_CustomMappingEditor_CustomName"
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxLength="50"
|
||||
TextChanged="CustomNameTextBox_TextChanged" />
|
||||
|
||||
<!-- Apply Scope -->
|
||||
<StackPanel Spacing="8">
|
||||
<ToggleSwitch
|
||||
x:Name="ApplyToAllToggle"
|
||||
x:Uid="PowerDisplay_CustomMappingEditor_ApplyToAll"
|
||||
IsOn="True"
|
||||
Toggled="ApplyToAllToggle_Toggled" />
|
||||
|
||||
<!-- Monitor Selection (shown when ApplyToAll is off) -->
|
||||
<ComboBox
|
||||
x:Name="MonitorComboBox"
|
||||
x:Uid="PowerDisplay_CustomMappingEditor_SelectMonitor"
|
||||
HorizontalAlignment="Stretch"
|
||||
DisplayMemberPath="DisplayName"
|
||||
ItemsSource="{x:Bind AvailableMonitors, Mode=OneWay}"
|
||||
SelectedValuePath="Id"
|
||||
SelectionChanged="MonitorComboBox_SelectionChanged"
|
||||
Visibility="{x:Bind ShowMonitorSelector, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,421 @@
|
||||
// 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;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,11 +63,51 @@
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<!-- Custom VCP Name Mappings -->
|
||||
<controls:SettingsGroup x:Uid="PowerDisplay_CustomVcpMappings_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="PowerDisplay_CustomVcpMappings"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.HasCustomVcpMappings, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomVcpMappings, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander.ItemTemplate>
|
||||
<DataTemplate x:DataType="pdmodels:CustomVcpValueMapping">
|
||||
<tkcontrols:SettingsCard Description="{x:Bind VcpCodeDisplayName}" Header="{x:Bind DisplaySummary}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
Click="EditCustomMapping_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=14}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="{x:Bind}"
|
||||
ToolTipService.ToolTip="Edit" />
|
||||
<Button
|
||||
Click="DeleteCustomMapping_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=14}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="{x:Bind}"
|
||||
ToolTipService.ToolTip="Delete" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</DataTemplate>
|
||||
</tkcontrols:SettingsExpander.ItemTemplate>
|
||||
|
||||
<!-- Add mapping button -->
|
||||
<Button x:Uid="PowerDisplay_AddCustomMappingButton" Click="AddCustomMapping_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="PowerDisplay_AddCustomMapping_Text" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="PowerDisplay_Profiles_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="PowerDisplay_QuickProfiles"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True"
|
||||
IsExpanded="{x:Bind ViewModel.HasProfiles, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander.ItemTemplate>
|
||||
<DataTemplate x:DataType="pdmodels:PowerDisplayProfile">
|
||||
|
||||
@@ -133,6 +133,65 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
return ProfileHelper.GenerateUniqueProfileName(existingNames, baseName);
|
||||
}
|
||||
|
||||
// Custom VCP Mapping event handlers
|
||||
private async void AddCustomMapping_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new CustomVcpMappingEditorDialog(ViewModel.Monitors);
|
||||
dialog.XamlRoot = this.XamlRoot;
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary && dialog.ResultMapping != null)
|
||||
{
|
||||
ViewModel.AddCustomVcpMapping(dialog.ResultMapping);
|
||||
}
|
||||
}
|
||||
|
||||
private async void EditCustomMapping_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button button || button.Tag is not CustomVcpValueMapping mapping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new CustomVcpMappingEditorDialog(ViewModel.Monitors);
|
||||
dialog.XamlRoot = this.XamlRoot;
|
||||
dialog.PreFillMapping(mapping);
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary && dialog.ResultMapping != null)
|
||||
{
|
||||
ViewModel.UpdateCustomVcpMapping(mapping, dialog.ResultMapping);
|
||||
}
|
||||
}
|
||||
|
||||
private async void DeleteCustomMapping_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button button || button.Tag is not CustomVcpValueMapping mapping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
XamlRoot = this.XamlRoot,
|
||||
Title = resourceLoader.GetString("PowerDisplay_CustomMapping_Delete_Title"),
|
||||
Content = resourceLoader.GetString("PowerDisplay_CustomMapping_Delete_Message"),
|
||||
PrimaryButtonText = resourceLoader.GetString("Yes"),
|
||||
CloseButtonText = resourceLoader.GetString("No"),
|
||||
DefaultButton = ContentDialogButton.Close,
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
ViewModel.DeleteCustomVcpMapping(mapping);
|
||||
}
|
||||
}
|
||||
|
||||
// Flag to prevent reentrant handling during programmatic checkbox changes
|
||||
private bool _isRestoringColorTempCheckbox;
|
||||
|
||||
|
||||
@@ -5995,6 +5995,69 @@ The break timer font matches the text font.</value>
|
||||
<data name="PowerDisplay_ShowIdentifyMonitorsButton.Description" xml:space="preserve">
|
||||
<value>Show or hide the identify monitors button in the Power Display flyout</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomVcpMappings_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Custom VCP Name Mappings</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomVcpMappings.Header" xml:space="preserve">
|
||||
<value>Custom name mappings</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomVcpMappings.Description" xml:space="preserve">
|
||||
<value>Define custom display names for color temperature presets and input sources</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_AddCustomMappingButton.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Add custom mapping</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_AddCustomMapping_Text.Text" xml:space="preserve">
|
||||
<value>Add mapping</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_Title" xml:space="preserve">
|
||||
<value>Custom VCP Name Mapping</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_VcpCode.Header" xml:space="preserve">
|
||||
<value>VCP Code</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_VcpCode_Name_0x14" xml:space="preserve">
|
||||
<value>Color Temperature</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_VcpCode_Name_0x60" xml:space="preserve">
|
||||
<value>Input Source</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_CustomName.Header" xml:space="preserve">
|
||||
<value>Custom Name</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_CustomName.PlaceholderText" xml:space="preserve">
|
||||
<value>Enter custom name</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_ValueComboBox.Header" xml:space="preserve">
|
||||
<value>Value</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_CustomValueOption" xml:space="preserve">
|
||||
<value>Custom value...</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_CustomValueInput.Header" xml:space="preserve">
|
||||
<value>Enter custom value (hex)</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_CustomValueInput.PlaceholderText" xml:space="preserve">
|
||||
<value>e.g., 0x11 or 17</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.Header" xml:space="preserve">
|
||||
<value>Apply to all monitors</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.OnContent" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.OffContent" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMappingEditor_SelectMonitor.Header" xml:space="preserve">
|
||||
<value>Select monitor</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMapping_Delete_Title" xml:space="preserve">
|
||||
<value>Delete custom mapping?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_CustomMapping_Delete_Message" xml:space="preserve">
|
||||
<value>This custom name mapping will be permanently removed.</value>
|
||||
</data>
|
||||
<data name="Hosts_Backup_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Backup</value>
|
||||
</data>
|
||||
|
||||
@@ -36,6 +36,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public PowerDisplayViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<PowerDisplaySettings> powerDisplaySettingsRepository, Func<string, int> ipcMSGCallBackFunc)
|
||||
{
|
||||
// Set up localized VCP code names for UI display
|
||||
VcpNames.LocalizedCodeNameProvider = GetLocalizedVcpCodeName;
|
||||
|
||||
// To obtain the general settings configurations of PowerToys Settings.
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
|
||||
@@ -56,9 +59,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
// Subscribe to collection changes for HasProfiles binding
|
||||
_profiles.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasProfiles));
|
||||
|
||||
// Load profiles
|
||||
LoadProfiles();
|
||||
|
||||
// Load custom VCP mappings
|
||||
LoadCustomVcpMappings();
|
||||
|
||||
// Listen for monitor refresh events from PowerDisplay.exe
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
Constants.RefreshPowerDisplayMonitorsEvent(),
|
||||
@@ -446,21 +455,28 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Profile-related fields
|
||||
private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
|
||||
|
||||
// Custom VCP mapping fields
|
||||
private ObservableCollection<CustomVcpValueMapping> _customVcpMappings;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets collection of available profiles (for button display)
|
||||
/// Gets collection of custom VCP value name mappings
|
||||
/// </summary>
|
||||
public ObservableCollection<PowerDisplayProfile> Profiles
|
||||
{
|
||||
get => _profiles;
|
||||
set
|
||||
{
|
||||
if (_profiles != value)
|
||||
{
|
||||
_profiles = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
public ObservableCollection<CustomVcpValueMapping> CustomVcpMappings => _customVcpMappings;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether there are any custom VCP mappings (for UI binding)
|
||||
/// </summary>
|
||||
public bool HasCustomVcpMappings => _customVcpMappings?.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets collection of available profiles (for button display)
|
||||
/// </summary>
|
||||
public ObservableCollection<PowerDisplayProfile> Profiles => _profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether there are any profiles (for UI binding)
|
||||
/// </summary>
|
||||
public bool HasProfiles => _profiles?.Count > 0;
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
@@ -646,6 +662,109 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load custom VCP mappings from settings
|
||||
/// </summary>
|
||||
private void LoadCustomVcpMappings()
|
||||
{
|
||||
List<CustomVcpValueMapping> mappings;
|
||||
try
|
||||
{
|
||||
mappings = _settings.Properties.CustomVcpMappings ?? new List<CustomVcpValueMapping>();
|
||||
Logger.LogInfo($"Loaded {mappings.Count} custom VCP mappings");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load custom VCP mappings: {ex.Message}");
|
||||
mappings = new List<CustomVcpValueMapping>();
|
||||
}
|
||||
|
||||
_customVcpMappings = new ObservableCollection<CustomVcpValueMapping>(mappings);
|
||||
_customVcpMappings.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasCustomVcpMappings));
|
||||
OnPropertyChanged(nameof(CustomVcpMappings));
|
||||
OnPropertyChanged(nameof(HasCustomVcpMappings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new custom VCP mapping.
|
||||
/// No duplicate checking - mappings are resolved by order (first match wins in VcpNames).
|
||||
/// </summary>
|
||||
public void AddCustomVcpMapping(CustomVcpValueMapping mapping)
|
||||
{
|
||||
if (mapping == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CustomVcpMappings.Add(mapping);
|
||||
Logger.LogInfo($"Added custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2} -> {mapping.CustomName}");
|
||||
SaveCustomVcpMappings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing custom VCP mapping
|
||||
/// </summary>
|
||||
public void UpdateCustomVcpMapping(CustomVcpValueMapping oldMapping, CustomVcpValueMapping newMapping)
|
||||
{
|
||||
if (oldMapping == null || newMapping == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = CustomVcpMappings.IndexOf(oldMapping);
|
||||
if (index >= 0)
|
||||
{
|
||||
CustomVcpMappings[index] = newMapping;
|
||||
Logger.LogInfo($"Updated custom VCP mapping at index {index}");
|
||||
SaveCustomVcpMappings();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a custom VCP mapping
|
||||
/// </summary>
|
||||
public void DeleteCustomVcpMapping(CustomVcpValueMapping mapping)
|
||||
{
|
||||
if (mapping == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (CustomVcpMappings.Remove(mapping))
|
||||
{
|
||||
Logger.LogInfo($"Deleted custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2}");
|
||||
SaveCustomVcpMappings();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save custom VCP mappings to settings
|
||||
/// </summary>
|
||||
private void SaveCustomVcpMappings()
|
||||
{
|
||||
_settings.Properties.CustomVcpMappings = CustomVcpMappings.ToList();
|
||||
NotifySettingsChanged();
|
||||
|
||||
// Signal PowerDisplay to reload settings
|
||||
SignalSettingsUpdated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides localized VCP code names for UI display.
|
||||
/// Looks for resource string with pattern "PowerDisplay_VcpCode_Name_0xXX".
|
||||
/// Returns null for unknown codes to use the default MCCS name.
|
||||
/// </summary>
|
||||
#nullable enable
|
||||
private static string? GetLocalizedVcpCodeName(byte vcpCode)
|
||||
{
|
||||
var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}";
|
||||
var localizedName = ResourceLoaderInstance.ResourceLoader.GetString(resourceKey);
|
||||
|
||||
// ResourceLoader returns empty string if key not found
|
||||
return string.IsNullOrEmpty(localizedName) ? null : localizedName;
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
{
|
||||
// Skip during initialization when SendConfigMSG is not yet set
|
||||
|
||||
Reference in New Issue
Block a user