CustomVcpMappingEditorDialog.xaml.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 #nullable enable 6 7 using System; 8 using System.Collections.Generic; 9 using System.Collections.ObjectModel; 10 using System.ComponentModel; 11 using System.Globalization; 12 using System.Linq; 13 using System.Runtime.CompilerServices; 14 using Microsoft.PowerToys.Settings.UI.Helpers; 15 using Microsoft.PowerToys.Settings.UI.Library; 16 using Microsoft.UI.Xaml; 17 using Microsoft.UI.Xaml.Controls; 18 using PowerDisplay.Common.Models; 19 using PowerDisplay.Common.Utils; 20 21 namespace Microsoft.PowerToys.Settings.UI.Views 22 { 23 /// <summary> 24 /// Dialog for creating/editing custom VCP value name mappings 25 /// </summary> 26 public sealed partial class CustomVcpMappingEditorDialog : ContentDialog, INotifyPropertyChanged 27 { 28 /// <summary> 29 /// Special value to indicate "Custom value" option in the ComboBox 30 /// </summary> 31 private const int CustomValueMarker = -1; 32 33 /// <summary> 34 /// Represents a selectable VCP value item in the Value ComboBox 35 /// </summary> 36 public class VcpValueItem 37 { 38 public int Value { get; set; } 39 40 public string DisplayName { get; set; } = string.Empty; 41 42 public bool IsCustomOption => Value == CustomValueMarker; 43 } 44 45 /// <summary> 46 /// Represents a selectable monitor item in the Monitor ComboBox 47 /// </summary> 48 public class MonitorItem 49 { 50 public string Id { get; set; } = string.Empty; 51 52 public string DisplayName { get; set; } = string.Empty; 53 } 54 55 private readonly IEnumerable<MonitorInfo>? _monitors; 56 private ObservableCollection<VcpValueItem> _availableValues = new(); 57 private ObservableCollection<MonitorItem> _availableMonitors = new(); 58 private byte _selectedVcpCode; 59 private int _selectedValue; 60 private string _customName = string.Empty; 61 private bool _canSave; 62 private bool _showCustomValueInput; 63 private bool _showMonitorSelector; 64 private int _customValueParsed; 65 private bool _applyToAll = true; 66 private string _selectedMonitorId = string.Empty; 67 private string _selectedMonitorName = string.Empty; 68 69 public CustomVcpMappingEditorDialog(IEnumerable<MonitorInfo>? monitors) 70 { 71 _monitors = monitors; 72 this.InitializeComponent(); 73 74 // Set localized strings for ContentDialog 75 var resourceLoader = ResourceLoaderInstance.ResourceLoader; 76 Title = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_Title"); 77 PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Save"); 78 CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel"); 79 80 // Set VCP code ComboBox items content dynamically using localized names 81 VcpCodeItem_0x14.Content = GetFormattedVcpCodeName(resourceLoader, 0x14); 82 VcpCodeItem_0x60.Content = GetFormattedVcpCodeName(resourceLoader, 0x60); 83 84 // Populate monitor list 85 PopulateMonitorList(); 86 87 // Default to Color Temperature (0x14) 88 VcpCodeComboBox.SelectedIndex = 0; 89 } 90 91 /// <summary> 92 /// Gets the result mapping after dialog closes with Primary button 93 /// </summary> 94 public CustomVcpValueMapping? ResultMapping { get; private set; } 95 96 /// <summary> 97 /// Gets the available values for the selected VCP code 98 /// </summary> 99 public ObservableCollection<VcpValueItem> AvailableValues 100 { 101 get => _availableValues; 102 private set 103 { 104 _availableValues = value; 105 OnPropertyChanged(); 106 } 107 } 108 109 /// <summary> 110 /// Gets the available monitors for selection 111 /// </summary> 112 public ObservableCollection<MonitorItem> AvailableMonitors 113 { 114 get => _availableMonitors; 115 private set 116 { 117 _availableMonitors = value; 118 OnPropertyChanged(); 119 } 120 } 121 122 /// <summary> 123 /// Gets a value indicating whether the dialog can be saved 124 /// </summary> 125 public bool CanSave 126 { 127 get => _canSave; 128 private set 129 { 130 if (_canSave != value) 131 { 132 _canSave = value; 133 OnPropertyChanged(); 134 } 135 } 136 } 137 138 /// <summary> 139 /// Gets a value indicating whether to show the custom value input TextBox 140 /// </summary> 141 public Visibility ShowCustomValueInput => _showCustomValueInput ? Visibility.Visible : Visibility.Collapsed; 142 143 /// <summary> 144 /// Gets a value indicating whether to show the monitor selector ComboBox 145 /// </summary> 146 public Visibility ShowMonitorSelector => _showMonitorSelector ? Visibility.Visible : Visibility.Collapsed; 147 148 private void SetShowCustomValueInput(bool value) 149 { 150 if (_showCustomValueInput != value) 151 { 152 _showCustomValueInput = value; 153 OnPropertyChanged(nameof(ShowCustomValueInput)); 154 } 155 } 156 157 private void SetShowMonitorSelector(bool value) 158 { 159 if (_showMonitorSelector != value) 160 { 161 _showMonitorSelector = value; 162 OnPropertyChanged(nameof(ShowMonitorSelector)); 163 } 164 } 165 166 private void PopulateMonitorList() 167 { 168 AvailableMonitors = new ObservableCollection<MonitorItem>( 169 _monitors?.Select(m => new MonitorItem { Id = m.Id, DisplayName = m.DisplayName }) 170 ?? Enumerable.Empty<MonitorItem>()); 171 172 if (AvailableMonitors.Count > 0) 173 { 174 MonitorComboBox.SelectedIndex = 0; 175 } 176 } 177 178 /// <summary> 179 /// Pre-fill the dialog with existing mapping data for editing 180 /// </summary> 181 public void PreFillMapping(CustomVcpValueMapping mapping) 182 { 183 if (mapping is null) 184 { 185 return; 186 } 187 188 // Select the VCP code 189 VcpCodeComboBox.SelectedIndex = mapping.VcpCode == 0x14 ? 0 : 1; 190 191 // Populate values for the selected VCP code 192 PopulateValuesForVcpCode(mapping.VcpCode); 193 194 // Try to select the value in the ComboBox 195 var matchingItem = AvailableValues.FirstOrDefault(v => !v.IsCustomOption && v.Value == mapping.Value); 196 if (matchingItem is not null) 197 { 198 ValueComboBox.SelectedItem = matchingItem; 199 } 200 else 201 { 202 // Value not found in list, select "Custom value" option and fill the TextBox 203 ValueComboBox.SelectedItem = AvailableValues.FirstOrDefault(v => v.IsCustomOption); 204 CustomValueTextBox.Text = $"0x{mapping.Value:X2}"; 205 _customValueParsed = mapping.Value; 206 } 207 208 // Set the custom name 209 CustomNameTextBox.Text = mapping.CustomName; 210 _customName = mapping.CustomName; 211 212 // Set apply scope 213 _applyToAll = mapping.ApplyToAll; 214 ApplyToAllToggle.IsOn = mapping.ApplyToAll; 215 SetShowMonitorSelector(!mapping.ApplyToAll); 216 217 // Select the target monitor if not applying to all 218 if (!mapping.ApplyToAll && !string.IsNullOrEmpty(mapping.TargetMonitorId)) 219 { 220 var targetMonitor = AvailableMonitors.FirstOrDefault(m => m.Id == mapping.TargetMonitorId); 221 if (targetMonitor is not null) 222 { 223 MonitorComboBox.SelectedItem = targetMonitor; 224 _selectedMonitorId = targetMonitor.Id; 225 _selectedMonitorName = targetMonitor.DisplayName; 226 } 227 } 228 229 UpdateCanSave(); 230 } 231 232 private void VcpCodeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 233 { 234 if (VcpCodeComboBox.SelectedItem is ComboBoxItem selectedItem && 235 selectedItem.Tag is string tagValue && 236 byte.TryParse(tagValue, out byte vcpCode)) 237 { 238 _selectedVcpCode = vcpCode; 239 PopulateValuesForVcpCode(vcpCode); 240 UpdateCanSave(); 241 } 242 } 243 244 private void PopulateValuesForVcpCode(byte vcpCode) 245 { 246 var values = new ObservableCollection<VcpValueItem>(); 247 var seenValues = new HashSet<int>(); 248 249 // Collect values from all monitors 250 if (_monitors is not null) 251 { 252 foreach (var monitor in _monitors) 253 { 254 if (monitor.VcpCodesFormatted is null) 255 { 256 continue; 257 } 258 259 // Find the VCP code entry 260 var vcpEntry = monitor.VcpCodesFormatted.FirstOrDefault(v => 261 !string.IsNullOrEmpty(v.Code) && 262 TryParseHexCode(v.Code, out int code) && 263 code == vcpCode); 264 265 if (vcpEntry?.ValueList is null) 266 { 267 continue; 268 } 269 270 // Add each value from this monitor 271 foreach (var valueInfo in vcpEntry.ValueList) 272 { 273 if (TryParseHexCode(valueInfo.Value, out int vcpValue) && !seenValues.Contains(vcpValue)) 274 { 275 seenValues.Add(vcpValue); 276 var displayName = !string.IsNullOrEmpty(valueInfo.Name) 277 ? $"{valueInfo.Name} (0x{vcpValue:X2})" 278 : VcpNames.GetFormattedValueName(vcpCode, vcpValue); 279 values.Add(new VcpValueItem 280 { 281 Value = vcpValue, 282 DisplayName = displayName, 283 }); 284 } 285 } 286 } 287 } 288 289 // If no values found from monitors, fall back to built-in values from VcpNames 290 if (values.Count == 0) 291 { 292 var builtInValues = VcpNames.GetValueMappings(vcpCode); 293 if (builtInValues is not null) 294 { 295 foreach (var kvp in builtInValues) 296 { 297 values.Add(new VcpValueItem 298 { 299 Value = kvp.Key, 300 DisplayName = $"{kvp.Value} (0x{kvp.Key:X2})", 301 }); 302 } 303 } 304 } 305 306 // Sort by value 307 var sortedValues = new ObservableCollection<VcpValueItem>(values.OrderBy(v => v.Value)); 308 309 // Add "Custom value" option at the end 310 var resourceLoader = ResourceLoaderInstance.ResourceLoader; 311 sortedValues.Add(new VcpValueItem 312 { 313 Value = CustomValueMarker, 314 DisplayName = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_CustomValueOption"), 315 }); 316 317 AvailableValues = sortedValues; 318 319 // Select first item if available 320 if (sortedValues.Count > 0) 321 { 322 ValueComboBox.SelectedIndex = 0; 323 } 324 } 325 326 private static bool TryParseHexCode(string? hex, out int result) 327 { 328 result = 0; 329 if (string.IsNullOrEmpty(hex)) 330 { 331 return false; 332 } 333 334 var cleanHex = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex; 335 return int.TryParse(cleanHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result); 336 } 337 338 private static string GetFormattedVcpCodeName(Windows.ApplicationModel.Resources.ResourceLoader resourceLoader, byte vcpCode) 339 { 340 var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}"; 341 var localizedName = resourceLoader.GetString(resourceKey); 342 var name = string.IsNullOrEmpty(localizedName) ? VcpNames.GetCodeName(vcpCode) : localizedName; 343 return $"{name} (0x{vcpCode:X2})"; 344 } 345 346 private void ValueComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 347 { 348 if (ValueComboBox.SelectedItem is VcpValueItem selectedItem) 349 { 350 SetShowCustomValueInput(selectedItem.IsCustomOption); 351 _selectedValue = selectedItem.IsCustomOption ? 0 : selectedItem.Value; 352 UpdateCanSave(); 353 } 354 } 355 356 private void CustomValueTextBox_TextChanged(object sender, TextChangedEventArgs e) 357 { 358 _customValueParsed = TryParseHexCode(CustomValueTextBox.Text?.Trim(), out int parsed) ? parsed : 0; 359 UpdateCanSave(); 360 } 361 362 private void CustomNameTextBox_TextChanged(object sender, TextChangedEventArgs e) 363 { 364 _customName = CustomNameTextBox.Text?.Trim() ?? string.Empty; 365 UpdateCanSave(); 366 } 367 368 private void ApplyToAllToggle_Toggled(object sender, RoutedEventArgs e) 369 { 370 _applyToAll = ApplyToAllToggle.IsOn; 371 SetShowMonitorSelector(!_applyToAll); 372 UpdateCanSave(); 373 } 374 375 private void MonitorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 376 { 377 if (MonitorComboBox.SelectedItem is MonitorItem selectedMonitor) 378 { 379 _selectedMonitorId = selectedMonitor.Id; 380 _selectedMonitorName = selectedMonitor.DisplayName; 381 UpdateCanSave(); 382 } 383 } 384 385 private void UpdateCanSave() 386 { 387 var hasValidValue = _showCustomValueInput 388 ? _customValueParsed > 0 389 : ValueComboBox.SelectedItem is VcpValueItem item && !item.IsCustomOption; 390 391 CanSave = _selectedVcpCode > 0 && 392 hasValidValue && 393 !string.IsNullOrWhiteSpace(_customName) && 394 (_applyToAll || !string.IsNullOrEmpty(_selectedMonitorId)); 395 } 396 397 private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) 398 { 399 if (CanSave) 400 { 401 int finalValue = _showCustomValueInput ? _customValueParsed : _selectedValue; 402 ResultMapping = new CustomVcpValueMapping 403 { 404 VcpCode = _selectedVcpCode, 405 Value = finalValue, 406 CustomName = _customName, 407 ApplyToAll = _applyToAll, 408 TargetMonitorId = _applyToAll ? string.Empty : _selectedMonitorId, 409 TargetMonitorName = _applyToAll ? string.Empty : _selectedMonitorName, 410 }; 411 } 412 } 413 414 public event PropertyChangedEventHandler? PropertyChanged; 415 416 private void OnPropertyChanged([CallerMemberName] string? propertyName = null) 417 { 418 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 419 } 420 } 421 }