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 }