PowerDisplayViewModel.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.Globalization; 9 using System.Linq; 10 using System.Runtime.CompilerServices; 11 using System.Text.Json; 12 13 using global::PowerToys.GPOWrapper; 14 using ManagedCommon; 15 using Microsoft.PowerToys.Settings.UI.Helpers; 16 using Microsoft.PowerToys.Settings.UI.Library; 17 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 18 using Microsoft.PowerToys.Settings.UI.Library.Interfaces; 19 using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; 20 using PowerDisplay.Common.Models; 21 using PowerDisplay.Common.Services; 22 using PowerDisplay.Common.Utils; 23 using PowerToys.Interop; 24 25 namespace Microsoft.PowerToys.Settings.UI.ViewModels 26 { 27 public partial class PowerDisplayViewModel : PageViewModelBase 28 { 29 protected override string ModuleName => PowerDisplaySettings.ModuleName; 30 31 private GeneralSettings GeneralSettingsConfig { get; set; } 32 33 private SettingsUtils SettingsUtils { get; set; } 34 35 public ButtonClickCommand LaunchEventHandler => new ButtonClickCommand(Launch); 36 37 public PowerDisplayViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<PowerDisplaySettings> powerDisplaySettingsRepository, Func<string, int> ipcMSGCallBackFunc) 38 { 39 // Set up localized VCP code names for UI display 40 VcpNames.LocalizedCodeNameProvider = GetLocalizedVcpCodeName; 41 42 // To obtain the general settings configurations of PowerToys Settings. 43 ArgumentNullException.ThrowIfNull(settingsRepository); 44 45 SettingsUtils = settingsUtils; 46 GeneralSettingsConfig = settingsRepository.SettingsConfig; 47 48 _settings = powerDisplaySettingsRepository.SettingsConfig; 49 50 InitializeEnabledValue(); 51 52 // Initialize monitors collection using property setter for proper subscription setup 53 var loadedMonitors = _settings.Properties.Monitors; 54 55 Logger.LogInfo($"[Constructor] Initializing with {loadedMonitors.Count} monitors from settings"); 56 57 Monitors = new ObservableCollection<MonitorInfo>(loadedMonitors); 58 59 // set the callback functions value to handle outgoing IPC message. 60 SendConfigMSG = ipcMSGCallBackFunc; 61 62 // Subscribe to collection changes for HasProfiles binding 63 _profiles.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasProfiles)); 64 65 // Load profiles 66 LoadProfiles(); 67 68 // Load custom VCP mappings 69 LoadCustomVcpMappings(); 70 71 // Listen for monitor refresh events from PowerDisplay.exe 72 NativeEventWaiter.WaitForEventLoop( 73 Constants.RefreshPowerDisplayMonitorsEvent(), 74 () => 75 { 76 Logger.LogInfo("Received refresh monitors event from PowerDisplay.exe"); 77 ReloadMonitorsFromSettings(); 78 }); 79 } 80 81 private GpoRuleConfigured _enabledGpoRuleConfiguration; 82 private bool _enabledStateIsGPOConfigured; 83 84 private void InitializeEnabledValue() 85 { 86 _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredPowerDisplayEnabledValue(); 87 if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) 88 { 89 // Get the enabled state from GPO 90 _enabledStateIsGPOConfigured = true; 91 _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; 92 } 93 else 94 { 95 _isEnabled = GeneralSettingsConfig.Enabled.PowerDisplay; 96 } 97 } 98 99 public bool IsEnabled 100 { 101 get => _isEnabled; 102 set 103 { 104 if (_enabledStateIsGPOConfigured) 105 { 106 // If it's GPO configured, shouldn't be able to change this state. 107 return; 108 } 109 110 if (_isEnabled != value) 111 { 112 _isEnabled = value; 113 OnPropertyChanged(nameof(IsEnabled)); 114 115 GeneralSettingsConfig.Enabled.PowerDisplay = value; 116 OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); 117 SendConfigMSG(outgoing.ToString()); 118 } 119 } 120 } 121 122 public bool IsEnabledGpoConfigured 123 { 124 get => _enabledStateIsGPOConfigured; 125 } 126 127 public bool RestoreSettingsOnStartup 128 { 129 get => _settings.Properties.RestoreSettingsOnStartup; 130 set => SetSettingsProperty(_settings.Properties.RestoreSettingsOnStartup, value, v => _settings.Properties.RestoreSettingsOnStartup = v); 131 } 132 133 public bool ShowSystemTrayIcon 134 { 135 get => _settings.Properties.ShowSystemTrayIcon; 136 set 137 { 138 if (SetSettingsProperty(_settings.Properties.ShowSystemTrayIcon, value, v => _settings.Properties.ShowSystemTrayIcon = v)) 139 { 140 // Explicitly signal PowerDisplay to refresh tray icon 141 // This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues 142 SignalSettingsUpdated(); 143 Logger.LogInfo($"ShowSystemTrayIcon changed to {value}"); 144 } 145 } 146 } 147 148 public bool ShowProfileSwitcher 149 { 150 get => _settings.Properties.ShowProfileSwitcher; 151 set 152 { 153 if (SetSettingsProperty(_settings.Properties.ShowProfileSwitcher, value, v => _settings.Properties.ShowProfileSwitcher = v)) 154 { 155 SignalSettingsUpdated(); 156 Logger.LogInfo($"ShowProfileSwitcher changed to {value}"); 157 } 158 } 159 } 160 161 public bool ShowIdentifyMonitorsButton 162 { 163 get => _settings.Properties.ShowIdentifyMonitorsButton; 164 set 165 { 166 if (SetSettingsProperty(_settings.Properties.ShowIdentifyMonitorsButton, value, v => _settings.Properties.ShowIdentifyMonitorsButton = v)) 167 { 168 SignalSettingsUpdated(); 169 Logger.LogInfo($"ShowIdentifyMonitorsButton changed to {value}"); 170 } 171 } 172 } 173 174 public HotkeySettings ActivationShortcut 175 { 176 get => _settings.Properties.ActivationShortcut; 177 set 178 { 179 if (SetSettingsProperty(_settings.Properties.ActivationShortcut, value, v => _settings.Properties.ActivationShortcut = v)) 180 { 181 // Signal PowerDisplay.exe to re-register the hotkey 182 EventHelper.SignalEvent(Constants.HotkeyUpdatedPowerDisplayEvent()); 183 Logger.LogInfo($"ActivationShortcut changed, signaled HotkeyUpdatedPowerDisplayEvent"); 184 } 185 } 186 } 187 188 public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings() 189 { 190 var hotkeysDict = new Dictionary<string, HotkeySettings[]> 191 { 192 [ModuleName] = [ActivationShortcut], 193 }; 194 195 return hotkeysDict; 196 } 197 198 /// <summary> 199 /// Gets or sets the delay in seconds before refreshing monitors after display changes. 200 /// </summary> 201 public int MonitorRefreshDelay 202 { 203 get => _settings.Properties.MonitorRefreshDelay; 204 set => SetSettingsProperty(_settings.Properties.MonitorRefreshDelay, value, v => _settings.Properties.MonitorRefreshDelay = v); 205 } 206 207 private readonly List<int> _monitorRefreshDelayOptions = new List<int> { 1, 2, 3, 5, 10 }; 208 209 public List<int> MonitorRefreshDelayOptions => _monitorRefreshDelayOptions; 210 211 public ObservableCollection<MonitorInfo> Monitors 212 { 213 get => _monitors; 214 set 215 { 216 if (_monitors != null) 217 { 218 _monitors.CollectionChanged -= Monitors_CollectionChanged; 219 UnsubscribeFromItemPropertyChanged(_monitors); 220 } 221 222 _monitors = value; 223 224 if (_monitors != null) 225 { 226 _monitors.CollectionChanged += Monitors_CollectionChanged; 227 SubscribeToItemPropertyChanged(_monitors); 228 } 229 230 OnPropertyChanged(nameof(Monitors)); 231 HasMonitors = _monitors?.Count > 0; 232 233 // Update TotalMonitorCount for dynamic DisplayName 234 UpdateTotalMonitorCount(); 235 } 236 } 237 238 public bool HasMonitors 239 { 240 get => _hasMonitors; 241 set 242 { 243 if (_hasMonitors != value) 244 { 245 _hasMonitors = value; 246 OnPropertyChanged(); 247 } 248 } 249 } 250 251 private void Monitors_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 252 { 253 SubscribeToItemPropertyChanged(e.NewItems?.Cast<MonitorInfo>()); 254 UnsubscribeFromItemPropertyChanged(e.OldItems?.Cast<MonitorInfo>()); 255 256 HasMonitors = _monitors.Count > 0; 257 _settings.Properties.Monitors = _monitors.ToList(); 258 NotifySettingsChanged(); 259 260 // Update TotalMonitorCount for dynamic DisplayName 261 UpdateTotalMonitorCount(); 262 } 263 264 /// <summary> 265 /// Update TotalMonitorCount on all monitors for dynamic DisplayName formatting. 266 /// When multiple monitors exist, DisplayName shows "Name N" format. 267 /// </summary> 268 private void UpdateTotalMonitorCount() 269 { 270 if (_monitors == null) 271 { 272 return; 273 } 274 275 var count = _monitors.Count; 276 foreach (var monitor in _monitors) 277 { 278 monitor.TotalMonitorCount = count; 279 } 280 } 281 282 [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Base class PageViewModelBase.Dispose() handles GC.SuppressFinalize")] 283 public override void Dispose() 284 { 285 // Unsubscribe from monitor property changes 286 UnsubscribeFromItemPropertyChanged(_monitors); 287 288 // Unsubscribe from collection changes 289 if (_monitors != null) 290 { 291 _monitors.CollectionChanged -= Monitors_CollectionChanged; 292 } 293 294 base.Dispose(); 295 } 296 297 /// <summary> 298 /// Subscribe to PropertyChanged events for items in the collection 299 /// </summary> 300 private void SubscribeToItemPropertyChanged(IEnumerable<MonitorInfo> items) 301 { 302 if (items != null) 303 { 304 foreach (var item in items) 305 { 306 item.PropertyChanged += OnMonitorPropertyChanged; 307 } 308 } 309 } 310 311 /// <summary> 312 /// Unsubscribe from PropertyChanged events for items in the collection 313 /// </summary> 314 private void UnsubscribeFromItemPropertyChanged(IEnumerable<MonitorInfo> items) 315 { 316 if (items != null) 317 { 318 foreach (var item in items) 319 { 320 item.PropertyChanged -= OnMonitorPropertyChanged; 321 } 322 } 323 } 324 325 /// <summary> 326 /// Handle PropertyChanged events from MonitorInfo objects 327 /// </summary> 328 private void OnMonitorPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) 329 { 330 if (sender is MonitorInfo monitor) 331 { 332 Logger.LogDebug($"[PowerDisplayViewModel] Monitor {monitor.Name} property {e.PropertyName} changed"); 333 } 334 335 // Update the settings object to keep it in sync 336 _settings.Properties.Monitors = _monitors.ToList(); 337 338 // Save settings when any monitor property changes 339 NotifySettingsChanged(); 340 341 // For feature visibility properties, explicitly signal PowerDisplay to refresh 342 // This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues 343 if (e.PropertyName == nameof(MonitorInfo.EnableContrast) || 344 e.PropertyName == nameof(MonitorInfo.EnableVolume) || 345 e.PropertyName == nameof(MonitorInfo.EnableInputSource) || 346 e.PropertyName == nameof(MonitorInfo.EnableRotation) || 347 e.PropertyName == nameof(MonitorInfo.EnableColorTemperature) || 348 e.PropertyName == nameof(MonitorInfo.EnablePowerState) || 349 e.PropertyName == nameof(MonitorInfo.IsHidden)) 350 { 351 SignalSettingsUpdated(); 352 } 353 } 354 355 /// <summary> 356 /// Signal PowerDisplay.exe that settings have been updated and need to be applied 357 /// </summary> 358 private void SignalSettingsUpdated() 359 { 360 EventHelper.SignalEvent(Constants.SettingsUpdatedPowerDisplayEvent()); 361 Logger.LogInfo("Signaled SettingsUpdatedPowerDisplayEvent for feature visibility change"); 362 } 363 364 public void Launch() 365 { 366 var actionMessage = new PowerDisplayActionMessage 367 { 368 Action = new PowerDisplayActionMessage.ActionData 369 { 370 PowerDisplay = new PowerDisplayActionMessage.PowerDisplayAction 371 { 372 ActionName = "Launch", 373 Value = string.Empty, 374 }, 375 }, 376 }; 377 378 SendConfigMSG(JsonSerializer.Serialize(actionMessage, SettingsSerializationContext.Default.PowerDisplayActionMessage)); 379 } 380 381 /// <summary> 382 /// Reload monitor list from settings file (called when PowerDisplay.exe signals monitor changes) 383 /// </summary> 384 private void ReloadMonitorsFromSettings() 385 { 386 try 387 { 388 Logger.LogInfo("Reloading monitors from settings file"); 389 390 // Read fresh settings from file 391 var updatedSettings = SettingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName); 392 var updatedMonitors = updatedSettings.Properties.Monitors; 393 394 Logger.LogInfo($"[ReloadMonitors] Loaded {updatedMonitors.Count} monitors from settings"); 395 396 // Update existing MonitorInfo objects instead of replacing the collection 397 // This preserves XAML x:Bind bindings which reference specific object instances 398 if (Monitors == null) 399 { 400 // First time initialization - create new collection 401 Monitors = new ObservableCollection<MonitorInfo>(updatedMonitors); 402 } 403 else 404 { 405 // Create a dictionary for quick lookup by Id 406 var updatedMonitorsDict = updatedMonitors.ToDictionary(m => m.Id, m => m); 407 408 // Update existing monitors or remove ones that no longer exist 409 for (int i = Monitors.Count - 1; i >= 0; i--) 410 { 411 var existingMonitor = Monitors[i]; 412 if (updatedMonitorsDict.TryGetValue(existingMonitor.Id, out var updatedMonitor) 413 && updatedMonitor != null) 414 { 415 // Monitor still exists - update its properties in place 416 Logger.LogInfo($"[ReloadMonitors] Updating existing monitor: {existingMonitor.Id}"); 417 existingMonitor.UpdateFrom(updatedMonitor); 418 419 updatedMonitorsDict.Remove(existingMonitor.Id); 420 } 421 else 422 { 423 // Monitor no longer exists - remove from collection 424 Logger.LogInfo($"[ReloadMonitors] Removing monitor: {existingMonitor.Id}"); 425 Monitors.RemoveAt(i); 426 } 427 } 428 429 // Add any new monitors that weren't in the existing collection 430 foreach (var newMonitor in updatedMonitorsDict.Values) 431 { 432 Logger.LogInfo($"[ReloadMonitors] Adding new monitor: {newMonitor.Id}"); 433 Monitors.Add(newMonitor); 434 } 435 } 436 437 // Update internal settings reference 438 _settings.Properties.Monitors = updatedMonitors; 439 440 Logger.LogInfo($"Successfully reloaded {updatedMonitors.Count} monitors"); 441 } 442 catch (Exception ex) 443 { 444 Logger.LogError($"Failed to reload monitors from settings: {ex.Message}"); 445 } 446 } 447 448 private Func<string, int> SendConfigMSG { get; } 449 450 private bool _isEnabled; 451 private PowerDisplaySettings _settings; 452 private ObservableCollection<MonitorInfo> _monitors; 453 private bool _hasMonitors; 454 455 // Profile-related fields 456 private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>(); 457 458 // Custom VCP mapping fields 459 private ObservableCollection<CustomVcpValueMapping> _customVcpMappings; 460 461 /// <summary> 462 /// Gets collection of custom VCP value name mappings 463 /// </summary> 464 public ObservableCollection<CustomVcpValueMapping> CustomVcpMappings => _customVcpMappings; 465 466 /// <summary> 467 /// Gets whether there are any custom VCP mappings (for UI binding) 468 /// </summary> 469 public bool HasCustomVcpMappings => _customVcpMappings?.Count > 0; 470 471 /// <summary> 472 /// Gets collection of available profiles (for button display) 473 /// </summary> 474 public ObservableCollection<PowerDisplayProfile> Profiles => _profiles; 475 476 /// <summary> 477 /// Gets whether there are any profiles (for UI binding) 478 /// </summary> 479 public bool HasProfiles => _profiles?.Count > 0; 480 481 public void RefreshEnabledState() 482 { 483 InitializeEnabledValue(); 484 OnPropertyChanged(nameof(IsEnabled)); 485 } 486 487 private bool SetSettingsProperty<T>(T currentValue, T newValue, Action<T> setter, [CallerMemberName] string propertyName = null) 488 { 489 if (EqualityComparer<T>.Default.Equals(currentValue, newValue)) 490 { 491 return false; 492 } 493 494 setter(newValue); 495 OnPropertyChanged(propertyName); 496 NotifySettingsChanged(); 497 return true; 498 } 499 500 /// <summary> 501 /// Load profiles from disk 502 /// </summary> 503 private void LoadProfiles() 504 { 505 try 506 { 507 var profilesData = ProfileService.LoadProfiles(); 508 509 // Load profile objects (no Custom - it's not a profile anymore) 510 Profiles.Clear(); 511 foreach (var profile in profilesData.Profiles) 512 { 513 Profiles.Add(profile); 514 } 515 516 Logger.LogInfo($"Loaded {Profiles.Count} profiles"); 517 } 518 catch (Exception ex) 519 { 520 Logger.LogError($"Failed to load profiles: {ex.Message}"); 521 Profiles.Clear(); 522 } 523 } 524 525 /// <summary> 526 /// Apply a profile to monitors 527 /// </summary> 528 public void ApplyProfile(PowerDisplayProfile profile) 529 { 530 try 531 { 532 if (profile == null || !profile.IsValid()) 533 { 534 Logger.LogWarning("Invalid profile"); 535 return; 536 } 537 538 Logger.LogInfo($"Applying profile: {profile.Name}"); 539 540 // Send custom action to trigger profile application 541 // The profile name is passed via Named Pipe IPC to PowerDisplay.exe 542 var actionMessage = new PowerDisplayActionMessage 543 { 544 Action = new PowerDisplayActionMessage.ActionData 545 { 546 PowerDisplay = new PowerDisplayActionMessage.PowerDisplayAction 547 { 548 ActionName = "ApplyProfile", 549 Value = profile.Name, 550 }, 551 }, 552 }; 553 554 SendConfigMSG(JsonSerializer.Serialize(actionMessage, SettingsSerializationContext.Default.PowerDisplayActionMessage)); 555 556 Logger.LogInfo($"Profile '{profile.Name}' applied successfully"); 557 } 558 catch (Exception ex) 559 { 560 Logger.LogError($"Failed to apply profile: {ex.Message}"); 561 } 562 } 563 564 /// <summary> 565 /// Create a new profile 566 /// </summary> 567 public void CreateProfile(PowerDisplayProfile profile) 568 { 569 try 570 { 571 if (profile == null || !profile.IsValid()) 572 { 573 Logger.LogWarning("Invalid profile"); 574 return; 575 } 576 577 Logger.LogInfo($"Creating profile: {profile.Name}"); 578 579 var profilesData = ProfileService.LoadProfiles(); 580 profilesData.SetProfile(profile); 581 ProfileService.SaveProfiles(profilesData); 582 583 // Reload profile list 584 LoadProfiles(); 585 586 // Signal PowerDisplay to reload profiles 587 SignalSettingsUpdated(); 588 589 Logger.LogInfo($"Profile '{profile.Name}' created successfully"); 590 } 591 catch (Exception ex) 592 { 593 Logger.LogError($"Failed to create profile: {ex.Message}"); 594 } 595 } 596 597 /// <summary> 598 /// Update an existing profile 599 /// </summary> 600 public void UpdateProfile(string oldName, PowerDisplayProfile newProfile) 601 { 602 try 603 { 604 if (newProfile == null || !newProfile.IsValid()) 605 { 606 Logger.LogWarning("Invalid profile"); 607 return; 608 } 609 610 Logger.LogInfo($"Updating profile: {oldName} -> {newProfile.Name}"); 611 612 var profilesData = ProfileService.LoadProfiles(); 613 614 // Remove old profile and add updated one 615 profilesData.RemoveProfile(oldName); 616 profilesData.SetProfile(newProfile); 617 ProfileService.SaveProfiles(profilesData); 618 619 // Reload profile list 620 LoadProfiles(); 621 622 // Signal PowerDisplay to reload profiles 623 SignalSettingsUpdated(); 624 625 Logger.LogInfo($"Profile updated to '{newProfile.Name}' successfully"); 626 } 627 catch (Exception ex) 628 { 629 Logger.LogError($"Failed to update profile: {ex.Message}"); 630 } 631 } 632 633 /// <summary> 634 /// Delete a profile 635 /// </summary> 636 public void DeleteProfile(string profileName) 637 { 638 try 639 { 640 if (string.IsNullOrEmpty(profileName)) 641 { 642 return; 643 } 644 645 Logger.LogInfo($"Deleting profile: {profileName}"); 646 647 var profilesData = ProfileService.LoadProfiles(); 648 profilesData.RemoveProfile(profileName); 649 ProfileService.SaveProfiles(profilesData); 650 651 // Reload profile list 652 LoadProfiles(); 653 654 // Signal PowerDisplay to reload profiles 655 SignalSettingsUpdated(); 656 657 Logger.LogInfo($"Profile '{profileName}' deleted successfully"); 658 } 659 catch (Exception ex) 660 { 661 Logger.LogError($"Failed to delete profile: {ex.Message}"); 662 } 663 } 664 665 /// <summary> 666 /// Load custom VCP mappings from settings 667 /// </summary> 668 private void LoadCustomVcpMappings() 669 { 670 List<CustomVcpValueMapping> mappings; 671 try 672 { 673 mappings = _settings.Properties.CustomVcpMappings ?? new List<CustomVcpValueMapping>(); 674 Logger.LogInfo($"Loaded {mappings.Count} custom VCP mappings"); 675 } 676 catch (Exception ex) 677 { 678 Logger.LogError($"Failed to load custom VCP mappings: {ex.Message}"); 679 mappings = new List<CustomVcpValueMapping>(); 680 } 681 682 _customVcpMappings = new ObservableCollection<CustomVcpValueMapping>(mappings); 683 _customVcpMappings.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasCustomVcpMappings)); 684 OnPropertyChanged(nameof(CustomVcpMappings)); 685 OnPropertyChanged(nameof(HasCustomVcpMappings)); 686 } 687 688 /// <summary> 689 /// Add a new custom VCP mapping. 690 /// No duplicate checking - mappings are resolved by order (first match wins in VcpNames). 691 /// </summary> 692 public void AddCustomVcpMapping(CustomVcpValueMapping mapping) 693 { 694 if (mapping == null) 695 { 696 return; 697 } 698 699 CustomVcpMappings.Add(mapping); 700 Logger.LogInfo($"Added custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2} -> {mapping.CustomName}"); 701 SaveCustomVcpMappings(); 702 } 703 704 /// <summary> 705 /// Update an existing custom VCP mapping 706 /// </summary> 707 public void UpdateCustomVcpMapping(CustomVcpValueMapping oldMapping, CustomVcpValueMapping newMapping) 708 { 709 if (oldMapping == null || newMapping == null) 710 { 711 return; 712 } 713 714 var index = CustomVcpMappings.IndexOf(oldMapping); 715 if (index >= 0) 716 { 717 CustomVcpMappings[index] = newMapping; 718 Logger.LogInfo($"Updated custom VCP mapping at index {index}"); 719 SaveCustomVcpMappings(); 720 } 721 } 722 723 /// <summary> 724 /// Delete a custom VCP mapping 725 /// </summary> 726 public void DeleteCustomVcpMapping(CustomVcpValueMapping mapping) 727 { 728 if (mapping == null) 729 { 730 return; 731 } 732 733 if (CustomVcpMappings.Remove(mapping)) 734 { 735 Logger.LogInfo($"Deleted custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2}"); 736 SaveCustomVcpMappings(); 737 } 738 } 739 740 /// <summary> 741 /// Save custom VCP mappings to settings 742 /// </summary> 743 private void SaveCustomVcpMappings() 744 { 745 _settings.Properties.CustomVcpMappings = CustomVcpMappings.ToList(); 746 NotifySettingsChanged(); 747 748 // Signal PowerDisplay to reload settings 749 SignalSettingsUpdated(); 750 } 751 752 /// <summary> 753 /// Provides localized VCP code names for UI display. 754 /// Looks for resource string with pattern "PowerDisplay_VcpCode_Name_0xXX". 755 /// Returns null for unknown codes to use the default MCCS name. 756 /// </summary> 757 #nullable enable 758 private static string? GetLocalizedVcpCodeName(byte vcpCode) 759 { 760 var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}"; 761 var localizedName = ResourceLoaderInstance.ResourceLoader.GetString(resourceKey); 762 763 // ResourceLoader returns empty string if key not found 764 return string.IsNullOrEmpty(localizedName) ? null : localizedName; 765 } 766 #nullable restore 767 768 private void NotifySettingsChanged() 769 { 770 // Skip during initialization when SendConfigMSG is not yet set 771 if (SendConfigMSG == null) 772 { 773 return; 774 } 775 776 // Persist locally first so settings survive even if the module DLL isn't loaded yet. 777 SettingsUtils.SaveSettings(_settings.ToJsonString(), PowerDisplaySettings.ModuleName); 778 779 // Using InvariantCulture as this is an IPC message 780 // This message will be intercepted by the runner, which passes the serialized JSON to 781 // PowerDisplay Module Interface's set_config() method, which then applies it in-process. 782 SendConfigMSG( 783 string.Format( 784 CultureInfo.InvariantCulture, 785 "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", 786 PowerDisplaySettings.ModuleName, 787 _settings.ToJsonString())); 788 } 789 } 790 }