/ src / settings-ui / Settings.UI.Library / MonitorInfo.cs
MonitorInfo.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  using System;
  6  using System.Collections.Generic;
  7  using System.Collections.ObjectModel;
  8  using System.Linq;
  9  using System.Text.Json.Serialization;
 10  using Microsoft.PowerToys.Settings.UI.Library.Helpers;
 11  using PowerDisplay.Common.Drivers;
 12  using PowerDisplay.Common.Models;
 13  using PowerDisplay.Common.Utils;
 14  
 15  namespace Microsoft.PowerToys.Settings.UI.Library
 16  {
 17      public class MonitorInfo : Observable
 18      {
 19          private string _name = string.Empty;
 20          private string _id = string.Empty;
 21          private string _communicationMethod = string.Empty;
 22          private int _currentBrightness;
 23          private int _colorTemperatureVcp = 0x05; // Default to 6500K preset (VCP 0x14 value)
 24          private int _contrast;
 25          private int _volume;
 26          private bool _isHidden;
 27          private bool _enableContrast;
 28          private bool _enableVolume;
 29          private bool _enableInputSource;
 30          private bool _enableRotation;
 31          private bool _enableColorTemperature;
 32          private bool _enablePowerState;
 33          private string _capabilitiesRaw = string.Empty;
 34          private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
 35          private int _monitorNumber;
 36          private int _totalMonitorCount;
 37  
 38          // Feature support status (determined from capabilities)
 39          private bool _supportsBrightness = true; // Brightness always shown even if unsupported
 40          private bool _supportsContrast;
 41          private bool _supportsColorTemperature;
 42          private bool _supportsVolume;
 43          private bool _supportsInputSource;
 44          private bool _supportsPowerState;
 45  
 46          // Cached color temperature presets (computed from VcpCodesFormatted)
 47          private ObservableCollection<ColorPresetItem> _availableColorPresetsCache;
 48          private ObservableCollection<ColorPresetItem> _colorPresetsForDisplayCache;
 49          private int _lastColorTemperatureVcpForCache = -1;
 50  
 51          /// <summary>
 52          /// Invalidates the color preset cache and notifies property changes.
 53          /// Call this when VcpCodesFormatted or SupportsColorTemperature changes.
 54          /// </summary>
 55          private void InvalidateColorPresetCache()
 56          {
 57              _availableColorPresetsCache = null;
 58              _colorPresetsForDisplayCache = null;
 59              _lastColorTemperatureVcpForCache = -1;
 60              OnPropertyChanged(nameof(ColorPresetsForDisplay));
 61          }
 62  
 63          public MonitorInfo()
 64          {
 65          }
 66  
 67          [JsonPropertyName("name")]
 68          public string Name
 69          {
 70              get => _name;
 71              set
 72              {
 73                  if (_name != value)
 74                  {
 75                      _name = value;
 76                      OnPropertyChanged();
 77                      OnPropertyChanged(nameof(DisplayName));
 78                  }
 79              }
 80          }
 81  
 82          /// <summary>
 83          /// Gets or sets the monitor number (Windows DISPLAY number, e.g., 1, 2, 3...).
 84          /// </summary>
 85          [JsonPropertyName("monitorNumber")]
 86          public int MonitorNumber
 87          {
 88              get => _monitorNumber;
 89              set
 90              {
 91                  if (_monitorNumber != value)
 92                  {
 93                      _monitorNumber = value;
 94                      OnPropertyChanged();
 95                      OnPropertyChanged(nameof(DisplayName));
 96                  }
 97              }
 98          }
 99  
100          /// <summary>
101          /// Gets or sets the total number of monitors (used for dynamic display name).
102          /// This is not serialized; it's set by the ViewModel.
103          /// </summary>
104          [JsonIgnore]
105          public int TotalMonitorCount
106          {
107              get => _totalMonitorCount;
108              set
109              {
110                  if (_totalMonitorCount != value)
111                  {
112                      _totalMonitorCount = value;
113                      OnPropertyChanged(nameof(DisplayName));
114                  }
115              }
116          }
117  
118          /// <summary>
119          /// Gets the display name - includes monitor number when multiple monitors exist.
120          /// Follows the same logic as PowerDisplay UI's MonitorViewModel.DisplayName.
121          /// </summary>
122          [JsonIgnore]
123          public string DisplayName
124          {
125              get
126              {
127                  // Show monitor number only when there are multiple monitors and MonitorNumber is valid
128                  if (TotalMonitorCount > 1 && MonitorNumber > 0)
129                  {
130                      return $"{Name} {MonitorNumber}";
131                  }
132  
133                  return Name;
134              }
135          }
136  
137          public string MonitorIconGlyph => CommunicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase)
138              ? "\uE7F8" // Laptop icon for WMI
139              : "\uE7F4"; // External monitor icon for DDC/CI and others
140  
141          [JsonPropertyName("id")]
142          public string Id
143          {
144              get => _id;
145              set
146              {
147                  if (_id != value)
148                  {
149                      _id = value;
150                      OnPropertyChanged();
151                  }
152              }
153          }
154  
155          [JsonPropertyName("communicationMethod")]
156          public string CommunicationMethod
157          {
158              get => _communicationMethod;
159              set
160              {
161                  if (_communicationMethod != value)
162                  {
163                      _communicationMethod = value;
164                      OnPropertyChanged();
165                  }
166              }
167          }
168  
169          [JsonPropertyName("currentBrightness")]
170          public int CurrentBrightness
171          {
172              get => _currentBrightness;
173              set
174              {
175                  if (_currentBrightness != value)
176                  {
177                      _currentBrightness = value;
178                      OnPropertyChanged();
179                  }
180              }
181          }
182  
183          /// <summary>
184          /// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
185          /// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
186          /// </summary>
187          [JsonPropertyName("colorTemperatureVcp")]
188          public int ColorTemperatureVcp
189          {
190              get => _colorTemperatureVcp;
191              set
192              {
193                  if (_colorTemperatureVcp != value)
194                  {
195                      _colorTemperatureVcp = value;
196                      OnPropertyChanged();
197                      OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Update display list when current value changes
198                  }
199              }
200          }
201  
202          /// <summary>
203          /// Gets or sets the current contrast value (0-100).
204          /// </summary>
205          [JsonPropertyName("contrast")]
206          public int Contrast
207          {
208              get => _contrast;
209              set
210              {
211                  if (_contrast != value)
212                  {
213                      _contrast = value;
214                      OnPropertyChanged();
215                  }
216              }
217          }
218  
219          /// <summary>
220          /// Gets or sets the current volume value (0-100).
221          /// </summary>
222          [JsonPropertyName("volume")]
223          public int Volume
224          {
225              get => _volume;
226              set
227              {
228                  if (_volume != value)
229                  {
230                      _volume = value;
231                      OnPropertyChanged();
232                  }
233              }
234          }
235  
236          [JsonPropertyName("isHidden")]
237          public bool IsHidden
238          {
239              get => _isHidden;
240              set
241              {
242                  if (_isHidden != value)
243                  {
244                      _isHidden = value;
245                      OnPropertyChanged();
246                  }
247              }
248          }
249  
250          [JsonPropertyName("enableContrast")]
251          public bool EnableContrast
252          {
253              get => _enableContrast;
254              set
255              {
256                  if (_enableContrast != value)
257                  {
258                      _enableContrast = value;
259                      OnPropertyChanged();
260                  }
261              }
262          }
263  
264          [JsonPropertyName("enableVolume")]
265          public bool EnableVolume
266          {
267              get => _enableVolume;
268              set
269              {
270                  if (_enableVolume != value)
271                  {
272                      _enableVolume = value;
273                      OnPropertyChanged();
274                  }
275              }
276          }
277  
278          [JsonPropertyName("enableInputSource")]
279          public bool EnableInputSource
280          {
281              get => _enableInputSource;
282              set
283              {
284                  if (_enableInputSource != value)
285                  {
286                      _enableInputSource = value;
287                      OnPropertyChanged();
288                  }
289              }
290          }
291  
292          [JsonPropertyName("enableRotation")]
293          public bool EnableRotation
294          {
295              get => _enableRotation;
296              set
297              {
298                  if (_enableRotation != value)
299                  {
300                      _enableRotation = value;
301                      OnPropertyChanged();
302                  }
303              }
304          }
305  
306          [JsonPropertyName("enableColorTemperature")]
307          public bool EnableColorTemperature
308          {
309              get => _enableColorTemperature;
310              set
311              {
312                  if (_enableColorTemperature != value)
313                  {
314                      _enableColorTemperature = value;
315                      OnPropertyChanged();
316                  }
317              }
318          }
319  
320          [JsonPropertyName("enablePowerState")]
321          public bool EnablePowerState
322          {
323              get => _enablePowerState;
324              set
325              {
326                  if (_enablePowerState != value)
327                  {
328                      _enablePowerState = value;
329                      OnPropertyChanged();
330                  }
331              }
332          }
333  
334          [JsonPropertyName("capabilitiesRaw")]
335          public string CapabilitiesRaw
336          {
337              get => _capabilitiesRaw;
338              set
339              {
340                  if (_capabilitiesRaw != value)
341                  {
342                      _capabilitiesRaw = value ?? string.Empty;
343                      OnPropertyChanged();
344                      OnPropertyChanged(nameof(HasCapabilities));
345                  }
346              }
347          }
348  
349          [JsonPropertyName("vcpCodesFormatted")]
350          public List<VcpCodeDisplayInfo> VcpCodesFormatted
351          {
352              get => _vcpCodesFormatted;
353              set
354              {
355                  var newValue = value ?? new List<VcpCodeDisplayInfo>();
356  
357                  // Only update if content actually changed (compare by VCP code list content)
358                  if (AreVcpCodesEqual(_vcpCodesFormatted, newValue))
359                  {
360                      return;
361                  }
362  
363                  _vcpCodesFormatted = newValue;
364                  OnPropertyChanged();
365                  InvalidateColorPresetCache();
366              }
367          }
368  
369          /// <summary>
370          /// Compare two VcpCodesFormatted lists for equality by content.
371          /// Returns true if both lists have the same VCP codes (by code value).
372          /// </summary>
373          private static bool AreVcpCodesEqual(List<VcpCodeDisplayInfo> list1, List<VcpCodeDisplayInfo> list2)
374          {
375              if (list1 == null && list2 == null)
376              {
377                  return true;
378              }
379  
380              if (list1 == null || list2 == null)
381              {
382                  return false;
383              }
384  
385              if (list1.Count != list2.Count)
386              {
387                  return false;
388              }
389  
390              // Compare by code values - order matters for our use case
391              for (int i = 0; i < list1.Count; i++)
392              {
393                  if (list1[i].Code != list2[i].Code)
394                  {
395                      return false;
396                  }
397  
398                  // Also compare ValueList count to detect preset changes
399                  var values1 = list1[i].ValueList;
400                  var values2 = list2[i].ValueList;
401                  if ((values1?.Count ?? 0) != (values2?.Count ?? 0))
402                  {
403                      return false;
404                  }
405              }
406  
407              return true;
408          }
409  
410          [JsonPropertyName("supportsBrightness")]
411          public bool SupportsBrightness
412          {
413              get => _supportsBrightness;
414              set
415              {
416                  if (_supportsBrightness != value)
417                  {
418                      _supportsBrightness = value;
419                      OnPropertyChanged();
420                  }
421              }
422          }
423  
424          [JsonPropertyName("supportsContrast")]
425          public bool SupportsContrast
426          {
427              get => _supportsContrast;
428              set
429              {
430                  if (_supportsContrast != value)
431                  {
432                      _supportsContrast = value;
433                      OnPropertyChanged();
434                  }
435              }
436          }
437  
438          [JsonPropertyName("supportsColorTemperature")]
439          public bool SupportsColorTemperature
440          {
441              get => _supportsColorTemperature;
442              set
443              {
444                  if (_supportsColorTemperature != value)
445                  {
446                      _supportsColorTemperature = value;
447                      OnPropertyChanged();
448                      InvalidateColorPresetCache(); // Notifies ColorPresetsForDisplay
449                  }
450              }
451          }
452  
453          [JsonPropertyName("supportsVolume")]
454          public bool SupportsVolume
455          {
456              get => _supportsVolume;
457              set
458              {
459                  if (_supportsVolume != value)
460                  {
461                      _supportsVolume = value;
462                      OnPropertyChanged();
463                  }
464              }
465          }
466  
467          [JsonPropertyName("supportsInputSource")]
468          public bool SupportsInputSource
469          {
470              get => _supportsInputSource;
471              set
472              {
473                  if (_supportsInputSource != value)
474                  {
475                      _supportsInputSource = value;
476                      OnPropertyChanged();
477                  }
478              }
479          }
480  
481          [JsonPropertyName("supportsPowerState")]
482          public bool SupportsPowerState
483          {
484              get => _supportsPowerState;
485              set
486              {
487                  if (_supportsPowerState != value)
488                  {
489                      _supportsPowerState = value;
490                      OnPropertyChanged();
491                  }
492              }
493          }
494  
495          /// <summary>
496          /// Gets available color temperature presets computed from VcpCodesFormatted (VCP code 0x14).
497          /// This is a computed property that parses the VCP capabilities data on-demand.
498          /// </summary>
499          private ObservableCollection<ColorPresetItem> AvailableColorPresets
500          {
501              get
502              {
503                  // Return cached value if available
504                  if (_availableColorPresetsCache != null)
505                  {
506                      return _availableColorPresetsCache;
507                  }
508  
509                  // Compute from VcpCodesFormatted
510                  _availableColorPresetsCache = ComputeAvailableColorPresets();
511                  return _availableColorPresetsCache;
512              }
513          }
514  
515          /// <summary>
516          /// Compute available color presets from VcpCodesFormatted (VCP code 0x14).
517          /// Uses ColorTemperatureHelper from PowerDisplay.Lib for shared computation logic.
518          /// </summary>
519          private ObservableCollection<ColorPresetItem> ComputeAvailableColorPresets()
520          {
521              // Check if color temperature is supported
522              if (!_supportsColorTemperature || _vcpCodesFormatted == null)
523              {
524                  return new ObservableCollection<ColorPresetItem>();
525              }
526  
527              // Find VCP code 0x14 (Color Temperature / Select Color Preset)
528              var colorTempVcp = _vcpCodesFormatted.FirstOrDefault(v =>
529                  !string.IsNullOrEmpty(v.Code) &&
530                  int.TryParse(
531                      v.Code.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? v.Code[2..] : v.Code,
532                      System.Globalization.NumberStyles.HexNumber,
533                      System.Globalization.CultureInfo.InvariantCulture,
534                      out int code) &&
535                  code == NativeConstants.VcpCodeSelectColorPreset);
536  
537              // No VCP 0x14 or no values
538              if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
539              {
540                  return new ObservableCollection<ColorPresetItem>();
541              }
542  
543              // Extract VCP values as tuples for ColorTemperatureHelper
544              var colorTempValues = colorTempVcp.ValueList
545                  .Select(valueInfo =>
546                  {
547                      var hex = valueInfo.Value;
548                      if (string.IsNullOrEmpty(hex))
549                      {
550                          return (VcpValue: 0, Name: valueInfo.Name);
551                      }
552  
553                      var cleanHex = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex;
554                      bool parsed = int.TryParse(cleanHex, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int vcpValue);
555                      return (VcpValue: parsed ? vcpValue : 0, Name: valueInfo.Name);
556                  })
557                  .Where(x => x.VcpValue > 0);
558  
559              // Use shared helper to compute presets, then convert to nested type for XAML compatibility
560              var basePresets = ColorTemperatureHelper.ComputeColorPresets(colorTempValues);
561              var presetList = basePresets.Select(p => new ColorPresetItem(p.VcpValue, p.DisplayName));
562              return new ObservableCollection<ColorPresetItem>(presetList);
563          }
564  
565          /// <summary>
566          /// Gets color presets for display in ComboBox, includes current value if not in preset list.
567          /// Uses caching to avoid recreating collections on every access.
568          /// </summary>
569          [JsonIgnore]
570          public ObservableCollection<ColorPresetItem> ColorPresetsForDisplay
571          {
572              get
573              {
574                  // Return cached value if available and color temperature hasn't changed
575                  if (_colorPresetsForDisplayCache != null && _lastColorTemperatureVcpForCache == _colorTemperatureVcp)
576                  {
577                      return _colorPresetsForDisplayCache;
578                  }
579  
580                  var presets = AvailableColorPresets;
581                  if (presets == null || presets.Count == 0)
582                  {
583                      _colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>();
584                      _lastColorTemperatureVcpForCache = _colorTemperatureVcp;
585                      return _colorPresetsForDisplayCache;
586                  }
587  
588                  // Check if current value is in the preset list
589                  var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperatureVcp);
590  
591                  if (currentValueInList)
592                  {
593                      // Current value is in the list, return as-is
594                      _colorPresetsForDisplayCache = presets;
595                  }
596                  else
597                  {
598                      // Current value is not in the preset list - add it at the beginning
599                      var displayList = new List<ColorPresetItem>();
600  
601                      // Add current value with "Custom" indicator using shared helper
602                      var displayName = ColorTemperatureHelper.FormatCustomColorTemperatureDisplayName(_colorTemperatureVcp);
603                      displayList.Add(new ColorPresetItem(_colorTemperatureVcp, displayName));
604  
605                      // Add all supported presets
606                      displayList.AddRange(presets);
607  
608                      _colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>(displayList);
609                  }
610  
611                  _lastColorTemperatureVcpForCache = _colorTemperatureVcp;
612                  return _colorPresetsForDisplayCache;
613              }
614          }
615  
616          [JsonIgnore]
617          public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
618  
619          [JsonIgnore]
620          public bool ShowCapabilitiesWarning => _communicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase);
621  
622          /// <summary>
623          /// Generate formatted text of all VCP codes for clipboard
624          /// </summary>
625          public string GetVcpCodesAsText()
626          {
627              if (_vcpCodesFormatted == null || _vcpCodesFormatted.Count == 0)
628              {
629                  return "No VCP codes detected";
630              }
631  
632              var lines = new List<string>();
633              lines.Add($"VCP Capabilities for: {_name}");
634              lines.Add($"Monitor ID: {_id}");
635              lines.Add(string.Empty);
636              lines.Add("Detected VCP Codes:");
637              lines.Add(new string('-', 50));
638  
639              foreach (var vcp in _vcpCodesFormatted)
640              {
641                  lines.Add(string.Empty);
642                  lines.Add(vcp.Title);
643                  if (vcp.HasValues)
644                  {
645                      lines.Add($"  {vcp.Values}");
646                  }
647              }
648  
649              lines.Add(string.Empty);
650              lines.Add(new string('-', 50));
651              lines.Add($"Total: {_vcpCodesFormatted.Count} VCP codes");
652  
653              return string.Join(System.Environment.NewLine, lines);
654          }
655  
656          /// <summary>
657          /// Update this monitor's properties from another MonitorInfo instance.
658          /// This preserves the object reference while updating all properties.
659          /// </summary>
660          /// <param name="other">The source MonitorInfo to copy properties from</param>
661          public void UpdateFrom(MonitorInfo other)
662          {
663              if (other == null)
664              {
665                  return;
666              }
667  
668              // Update all properties that can change
669              Name = other.Name;
670              Id = other.Id;
671              CommunicationMethod = other.CommunicationMethod;
672              CurrentBrightness = other.CurrentBrightness;
673              Contrast = other.Contrast;
674              Volume = other.Volume;
675              ColorTemperatureVcp = other.ColorTemperatureVcp;
676              IsHidden = other.IsHidden;
677              EnableContrast = other.EnableContrast;
678              EnableVolume = other.EnableVolume;
679              EnableInputSource = other.EnableInputSource;
680              EnableRotation = other.EnableRotation;
681              EnableColorTemperature = other.EnableColorTemperature;
682              EnablePowerState = other.EnablePowerState;
683              CapabilitiesRaw = other.CapabilitiesRaw;
684              VcpCodesFormatted = other.VcpCodesFormatted;
685              SupportsBrightness = other.SupportsBrightness;
686              SupportsContrast = other.SupportsContrast;
687              SupportsColorTemperature = other.SupportsColorTemperature;
688              SupportsVolume = other.SupportsVolume;
689              SupportsInputSource = other.SupportsInputSource;
690              SupportsPowerState = other.SupportsPowerState;
691              MonitorNumber = other.MonitorNumber;
692          }
693      }
694  }