/ src / settings-ui / Settings.UI / SettingsXAML / Views / CustomVcpMappingEditorDialog.xaml.cs
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  }