Add per-monitor custom VCP value mapping support

Custom VCP value name mappings can now be applied globally or to a specific monitor.
- Added ApplyToAll and TargetMonitorId to mapping models and UI.
- Updated VcpNames utility to resolve mappings by monitor.
- UI now allows selecting mapping scope and target monitor.
- Display names and settings refresh when mappings change.
- Expander open state and resource strings updated for new features.
This enables more flexible and precise VCP value naming in multi-monitor setups.
This commit is contained in:
Yu Leng
2026-02-04 16:07:09 +08:00
parent f11d36c9ae
commit 2fb81e3e9b
11 changed files with 292 additions and 191 deletions

View File

@@ -29,5 +29,19 @@ namespace PowerDisplay.Common.Models
/// </summary>
[JsonPropertyName("customName")]
public string CustomName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether this mapping applies to all monitors.
/// When true, the mapping is applied globally. When false, only applies to TargetMonitorId.
/// </summary>
[JsonPropertyName("applyToAll")]
public bool ApplyToAll { get; set; } = true;
/// <summary>
/// Gets or sets the target monitor ID when ApplyToAll is false.
/// This is the monitor's unique identifier.
/// </summary>
[JsonPropertyName("targetMonitorId")]
public string TargetMonitorId { get; set; } = string.Empty;
}
}

View File

@@ -391,6 +391,16 @@ namespace PowerDisplay.Common.Utils
},
};
/// <summary>
/// Get all known values for a VCP code
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <returns>Dictionary of value to name mappings, or null if no mappings exist</returns>
public static IReadOnlyDictionary<int, string>? GetValueMappings(byte vcpCode)
{
return ValueNames.TryGetValue(vcpCode, out var values) ? values : null;
}
/// <summary>
/// Get human-readable name for a VCP value
/// </summary>
@@ -430,18 +440,26 @@ namespace PowerDisplay.Common.Utils
/// <summary>
/// Get human-readable name for a VCP value with custom mapping support.
/// Custom mappings take priority over built-in mappings.
/// Monitor ID is required to properly filter monitor-specific mappings.
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <param name="customMappings">Optional custom mappings that take priority</param>
/// <param name="monitorId">Monitor ID to filter mappings</param>
/// <returns>Name string like "sRGB" or null if unknown</returns>
public static string? GetValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? customMappings)
public static string? GetValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? customMappings, string monitorId)
{
// 1. Priority: Check custom mappings first
if (customMappings != null)
{
// Find a matching custom mapping:
// - ApplyToAll = true (global), OR
// - ApplyToAll = false AND TargetMonitorId matches the given monitorId
var custom = customMappings.FirstOrDefault(m =>
m.VcpCode == vcpCode && m.Value == value);
m.VcpCode == vcpCode &&
m.Value == value &&
(m.ApplyToAll || (!m.ApplyToAll && m.TargetMonitorId == monitorId)));
if (custom != null && !string.IsNullOrEmpty(custom.CustomName))
{
return custom.CustomName;
@@ -455,14 +473,16 @@ namespace PowerDisplay.Common.Utils
/// <summary>
/// Get formatted display name for a VCP value with custom mapping support.
/// Custom mappings take priority over built-in mappings.
/// Monitor ID is required to properly filter monitor-specific mappings.
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <param name="customMappings">Optional custom mappings that take priority</param>
/// <param name="monitorId">Monitor ID to filter mappings</param>
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
public static string GetFormattedValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? customMappings)
public static string GetFormattedValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? customMappings, string monitorId)
{
var name = GetValueName(vcpCode, value, customMappings);
var name = GetValueName(vcpCode, value, customMappings, monitorId);
if (name != null)
{
return $"{name} (0x{value:X2})";

View File

@@ -43,6 +43,10 @@ public partial class MainViewModel
// UpdateMonitorList already handles filtering hidden monitors
UpdateMonitorList(_monitorManager.Monitors, isInitialLoad: false);
// Reload UI display settings first (includes custom VCP mappings)
// Must be loaded before ApplyUIConfiguration so names are available for UI refresh
LoadUIDisplaySettings();
// Apply UI configuration changes only (feature visibility toggles, etc.)
// Hardware parameters (brightness, color temperature) are applied via custom actions
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
@@ -51,8 +55,11 @@ public partial class MainViewModel
// Reload profiles in case they were added/updated/deleted in Settings UI
LoadProfiles();
// Reload UI display settings (profile switcher, identify button, color temp switcher)
LoadUIDisplaySettings();
// Notify MonitorViewModels to refresh their custom VCP name displays
foreach (var monitor in Monitors)
{
monitor.RefreshCustomVcpNames();
}
}
catch (Exception ex)
{

View File

@@ -416,6 +416,8 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
VcpCode = m.VcpCode,
Value = m.Value,
CustomName = m.CustomName,
ApplyToAll = m.ApplyToAll,
TargetMonitorId = m.TargetMonitorId,
}).ToList();
Logger.LogInfo($"[Settings] Loaded {CustomVcpMappings.Count} custom VCP mappings");
}

View File

@@ -469,7 +469,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// Uses custom mappings if available, otherwise falls back to built-in names.
/// </summary>
public string ColorTemperaturePresetName =>
Common.Utils.VcpNames.GetFormattedValueName(0x14, _monitor.CurrentColorTemperature, _mainViewModel?.CustomVcpMappings);
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
@@ -549,7 +549,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableColorPresets = presetValues.Select(value => new ColorTemperatureItem
{
VcpValue = value,
DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value, _mainViewModel?.CustomVcpMappings),
DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value, _mainViewModel?.CustomVcpMappings, _monitor.Id),
IsSelected = value == _monitor.CurrentColorTemperature,
MonitorId = _monitor.Id,
}).ToList();
@@ -572,7 +572,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// Uses custom mappings if available, otherwise falls back to built-in names.
/// </summary>
public string CurrentInputSourceName =>
Common.Utils.VcpNames.GetValueName(0x60, _monitor.CurrentInputSource, _mainViewModel?.CustomVcpMappings)
Common.Utils.VcpNames.GetValueName(0x60, _monitor.CurrentInputSource, _mainViewModel?.CustomVcpMappings, _monitor.Id)
?? $"Source 0x{_monitor.CurrentInputSource:X2}";
private List<InputSourceItem>? _availableInputSources;
@@ -608,7 +608,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableInputSources = supportedSources.Select(value => new InputSourceItem
{
Value = value,
Name = Common.Utils.VcpNames.GetValueName(0x60, value, _mainViewModel?.CustomVcpMappings) ?? $"Source 0x{value:X2}",
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();
@@ -616,6 +616,23 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
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>

View File

@@ -31,6 +31,26 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("customName")]
public string CustomName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether this mapping applies to all monitors.
/// When true, the mapping is applied globally. When false, only applies to TargetMonitorId.
/// </summary>
[JsonPropertyName("applyToAll")]
public bool ApplyToAll { get; set; } = true;
/// <summary>
/// Gets or sets the target monitor ID when ApplyToAll is false.
/// This is the monitor's unique identifier.
/// </summary>
[JsonPropertyName("targetMonitorId")]
public string TargetMonitorId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the target monitor display name (for UI display only, not serialized).
/// </summary>
[JsonIgnore]
public string TargetMonitorName { get; set; } = string.Empty;
/// <summary>
/// Gets the display name for the VCP code (for UI display).
/// </summary>
@@ -50,9 +70,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// <summary>
/// Gets a summary string for display in the UI list.
/// Format: "VcpCodeName: OriginalValue → CustomName"
/// Format: "OriginalValue → CustomName" or "OriginalValue → CustomName (MonitorName)"
/// </summary>
[JsonIgnore]
public string DisplaySummary => $"{VcpNames.GetValueName(VcpCode, Value) ?? $"0x{Value:X2}"} → {CustomName}";
public string DisplaySummary
{
get
{
var baseSummary = $"{VcpNames.GetValueName(VcpCode, Value) ?? $"0x{Value:X2}"} → {CustomName}";
if (!ApplyToAll && !string.IsNullOrEmpty(TargetMonitorName))
{
return $"{baseSummary} ({TargetMonitorName})";
}
return baseSummary;
}
}
}
}

View File

@@ -6,7 +6,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Width="400"
MinWidth="400"
CloseButtonClick="ContentDialog_CloseButtonClick"
DefaultButton="Primary"
IsPrimaryButtonEnabled="{x:Bind CanSave, Mode=OneWay}"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
@@ -52,5 +51,25 @@
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>

View File

@@ -41,19 +41,29 @@ namespace Microsoft.PowerToys.Settings.UI.Views
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;
public CustomVcpMappingEditorDialog()
: this(null)
{
}
private bool _applyToAll = true;
private string _selectedMonitorId = string.Empty;
private string _selectedMonitorName = string.Empty;
public CustomVcpMappingEditorDialog(IEnumerable<MonitorInfo>? monitors)
{
@@ -66,6 +76,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Save");
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel");
// Populate monitor list
PopulateMonitorList();
// Default to Color Temperature (0x14)
VcpCodeComboBox.SelectedIndex = 0;
}
@@ -88,6 +101,19 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
/// <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>
@@ -107,17 +133,40 @@ namespace Microsoft.PowerToys.Settings.UI.Views
/// <summary>
/// Gets a value indicating whether to show the custom value input TextBox
/// </summary>
public Visibility ShowCustomValueInput
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)
{
get => _showCustomValueInput ? Visibility.Visible : Visibility.Collapsed;
private set
if (_showCustomValueInput != value)
{
var newValue = value == Visibility.Visible;
if (_showCustomValueInput != newValue)
{
_showCustomValueInput = newValue;
OnPropertyChanged();
}
_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;
}
}
@@ -138,27 +187,15 @@ namespace Microsoft.PowerToys.Settings.UI.Views
PopulateValuesForVcpCode(mapping.VcpCode);
// Try to select the value in the ComboBox
bool foundInList = false;
foreach (var item in AvailableValues)
var matchingItem = AvailableValues.FirstOrDefault(v => !v.IsCustomOption && v.Value == mapping.Value);
if (matchingItem != null)
{
if (!item.IsCustomOption && item.Value == mapping.Value)
{
ValueComboBox.SelectedItem = item;
foundInList = true;
break;
}
ValueComboBox.SelectedItem = matchingItem;
}
// If value not found in list, select "Custom value" option and fill the TextBox
if (!foundInList)
else
{
// Select the "Custom value" option (last item)
var customOption = AvailableValues.FirstOrDefault(v => v.IsCustomOption);
if (customOption != null)
{
ValueComboBox.SelectedItem = customOption;
}
// 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;
}
@@ -167,6 +204,23 @@ namespace Microsoft.PowerToys.Settings.UI.Views
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 != null)
{
MonitorComboBox.SelectedItem = targetMonitor;
_selectedMonitorId = targetMonitor.Id;
_selectedMonitorName = targetMonitor.DisplayName;
}
}
UpdateCanSave();
}
@@ -227,23 +281,20 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
// If no values found from monitors, fall back to built-in values
// If no values found from monitors, fall back to built-in values from VcpNames
if (values.Count == 0)
{
Dictionary<int, string> builtInValues = vcpCode switch
var builtInValues = VcpNames.GetValueMappings(vcpCode);
if (builtInValues != null)
{
0x14 => GetColorTemperatureValues(),
0x60 => GetInputSourceValues(),
_ => new Dictionary<int, string>(),
};
foreach (var kvp in builtInValues)
{
values.Add(new VcpValueItem
foreach (var kvp in builtInValues)
{
Value = kvp.Key,
DisplayName = $"{kvp.Value} (0x{kvp.Key:X2})",
});
values.Add(new VcpValueItem
{
Value = kvp.Key,
DisplayName = $"{kvp.Value} (0x{kvp.Key:X2})",
});
}
}
}
@@ -279,85 +330,19 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return int.TryParse(cleanHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result);
}
private static Dictionary<int, string> GetColorTemperatureValues()
{
return new Dictionary<int, string>
{
{ 0x01, "sRGB" },
{ 0x02, "Display Native" },
{ 0x03, "4000K" },
{ 0x04, "5000K" },
{ 0x05, "6500K" },
{ 0x06, "7500K" },
{ 0x07, "8200K" },
{ 0x08, "9300K" },
{ 0x09, "10000K" },
{ 0x0A, "11500K" },
{ 0x0B, "User 1" },
{ 0x0C, "User 2" },
{ 0x0D, "User 3" },
};
}
private static Dictionary<int, string> GetInputSourceValues()
{
return new Dictionary<int, string>
{
{ 0x01, "VGA-1" },
{ 0x02, "VGA-2" },
{ 0x03, "DVI-1" },
{ 0x04, "DVI-2" },
{ 0x05, "Composite Video 1" },
{ 0x06, "Composite Video 2" },
{ 0x07, "S-Video-1" },
{ 0x08, "S-Video-2" },
{ 0x09, "Tuner-1" },
{ 0x0A, "Tuner-2" },
{ 0x0B, "Tuner-3" },
{ 0x0C, "Component Video 1" },
{ 0x0D, "Component Video 2" },
{ 0x0E, "Component Video 3" },
{ 0x0F, "DisplayPort-1" },
{ 0x10, "DisplayPort-2" },
{ 0x11, "HDMI-1" },
{ 0x12, "HDMI-2" },
{ 0x1B, "USB-C" },
};
}
private void ValueComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ValueComboBox.SelectedItem is VcpValueItem selectedItem)
{
if (selectedItem.IsCustomOption)
{
// Show custom value input
ShowCustomValueInput = Visibility.Visible;
_selectedValue = 0; // Will be set from TextBox
}
else
{
// Hide custom value input and use selected value
ShowCustomValueInput = Visibility.Collapsed;
_selectedValue = selectedItem.Value;
}
SetShowCustomValueInput(selectedItem.IsCustomOption);
_selectedValue = selectedItem.IsCustomOption ? 0 : selectedItem.Value;
UpdateCanSave();
}
}
private void CustomValueTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var text = CustomValueTextBox.Text?.Trim() ?? string.Empty;
if (TryParseHexCode(text, out int parsed))
{
_customValueParsed = parsed;
}
else
{
_customValueParsed = 0;
}
_customValueParsed = TryParseHexCode(CustomValueTextBox.Text?.Trim(), out int parsed) ? parsed : 0;
UpdateCanSave();
}
@@ -367,21 +352,33 @@ namespace Microsoft.PowerToys.Settings.UI.Views
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()
{
bool hasValidValue;
if (_showCustomValueInput)
{
hasValidValue = _customValueParsed > 0;
}
else
{
hasValidValue = ValueComboBox.SelectedItem is VcpValueItem item && !item.IsCustomOption;
}
var hasValidValue = _showCustomValueInput
? _customValueParsed > 0
: ValueComboBox.SelectedItem is VcpValueItem item && !item.IsCustomOption;
CanSave = _selectedVcpCode > 0 &&
hasValidValue &&
!string.IsNullOrWhiteSpace(_customName);
!string.IsNullOrWhiteSpace(_customName) &&
(_applyToAll || !string.IsNullOrEmpty(_selectedMonitorId));
}
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
@@ -394,15 +391,13 @@ namespace Microsoft.PowerToys.Settings.UI.Views
VcpCode = _selectedVcpCode,
Value = finalValue,
CustomName = _customName,
ApplyToAll = _applyToAll,
TargetMonitorId = _applyToAll ? string.Empty : _selectedMonitorId,
TargetMonitorName = _applyToAll ? string.Empty : _selectedMonitorName,
};
}
}
private void ContentDialog_CloseButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
ResultMapping = null;
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)

View File

@@ -68,7 +68,7 @@
<tkcontrols:SettingsExpander
x:Uid="PowerDisplay_CustomVcpMappings"
HeaderIcon="{ui:FontIcon Glyph=&#xE70F;}"
IsExpanded="True"
IsExpanded="{x:Bind ViewModel.HasCustomVcpMappings, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.CustomVcpMappings, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="library:CustomVcpValueMapping">
@@ -107,7 +107,7 @@
<tkcontrols:SettingsExpander
x:Uid="PowerDisplay_QuickProfiles"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsExpanded="True"
IsExpanded="{x:Bind ViewModel.HasProfiles, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="pdmodels:PowerDisplayProfile">

View File

@@ -6037,6 +6037,18 @@ The break timer font matches the text font.</value>
<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>

View File

@@ -56,6 +56,9 @@ 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();
@@ -450,39 +453,27 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
// Custom VCP mapping fields
private ObservableCollection<Library.CustomVcpValueMapping> _customVcpMappings = new ObservableCollection<Library.CustomVcpValueMapping>();
private ObservableCollection<Library.CustomVcpValueMapping> _customVcpMappings;
/// <summary>
/// Gets or sets collection of custom VCP value name mappings
/// Gets collection of custom VCP value name mappings
/// </summary>
public ObservableCollection<Library.CustomVcpValueMapping> CustomVcpMappings
{
get => _customVcpMappings;
set
{
if (_customVcpMappings != value)
{
_customVcpMappings = value;
OnPropertyChanged();
}
}
}
public ObservableCollection<Library.CustomVcpValueMapping> CustomVcpMappings => _customVcpMappings;
/// <summary>
/// Gets or sets collection of available profiles (for button display)
/// Gets whether there are any custom VCP mappings (for UI binding)
/// </summary>
public ObservableCollection<PowerDisplayProfile> Profiles
{
get => _profiles;
set
{
if (_profiles != value)
{
_profiles = value;
OnPropertyChanged();
}
}
}
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()
{
@@ -676,18 +667,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
try
{
var mappings = _settings.Properties.CustomVcpMappings ?? new List<Library.CustomVcpValueMapping>();
CustomVcpMappings = new ObservableCollection<Library.CustomVcpValueMapping>(mappings);
_customVcpMappings = new ObservableCollection<Library.CustomVcpValueMapping>(mappings);
_customVcpMappings.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasCustomVcpMappings));
OnPropertyChanged(nameof(CustomVcpMappings));
OnPropertyChanged(nameof(HasCustomVcpMappings));
Logger.LogInfo($"Loaded {CustomVcpMappings.Count} custom VCP mappings");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load custom VCP mappings: {ex.Message}");
CustomVcpMappings = new ObservableCollection<Library.CustomVcpValueMapping>();
_customVcpMappings = new ObservableCollection<Library.CustomVcpValueMapping>();
_customVcpMappings.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasCustomVcpMappings));
OnPropertyChanged(nameof(CustomVcpMappings));
OnPropertyChanged(nameof(HasCustomVcpMappings));
}
}
/// <summary>
/// Add a new custom VCP mapping
/// Add a new custom VCP mapping.
/// No duplicate checking - mappings are resolved by order (first match wins in VcpNames).
/// </summary>
public void AddCustomVcpMapping(Library.CustomVcpValueMapping mapping)
{
@@ -696,23 +694,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return;
}
// Check if a mapping for the same VcpCode+Value already exists
var existing = CustomVcpMappings.FirstOrDefault(m =>
m.VcpCode == mapping.VcpCode && m.Value == mapping.Value);
if (existing != null)
{
// Update the existing mapping's custom name
existing.CustomName = mapping.CustomName;
Logger.LogInfo($"Updated existing custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2}");
}
else
{
// Add new mapping
CustomVcpMappings.Add(mapping);
Logger.LogInfo($"Added custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2} -> {mapping.CustomName}");
}
CustomVcpMappings.Add(mapping);
Logger.LogInfo($"Added custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2} -> {mapping.CustomName}");
SaveCustomVcpMappings();
}