AdvancedPasteViewModel.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.Collections.Specialized; 9 using System.ComponentModel; 10 using System.Globalization; 11 using System.IO.Abstractions; 12 using System.Linq; 13 using System.Runtime.Versioning; 14 using System.Text.Json; 15 16 using global::PowerToys.GPOWrapper; 17 using Microsoft.PowerToys.Settings.UI.Helpers; 18 using Microsoft.PowerToys.Settings.UI.Library; 19 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 20 using Microsoft.PowerToys.Settings.UI.Library.Interfaces; 21 using Microsoft.PowerToys.Settings.UI.Library.Utilities; 22 using Microsoft.PowerToys.Settings.UI.SerializationContext; 23 using Microsoft.UI.Dispatching; 24 using Microsoft.Win32; 25 using Windows.Security.Credentials; 26 27 namespace Microsoft.PowerToys.Settings.UI.ViewModels 28 { 29 public partial class AdvancedPasteViewModel : PageViewModelBase 30 { 31 private static readonly HashSet<string> WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"]; 32 33 private bool _disposed; 34 private PasteAIProviderDefinition _pasteAIProviderDraft; 35 private PasteAIProviderDefinition _editingPasteAIProvider; 36 37 protected override string ModuleName => AdvancedPasteSettings.ModuleName; 38 39 private GeneralSettings GeneralSettingsConfig { get; set; } 40 41 private readonly SettingsUtils _settingsUtils; 42 43 private readonly AdvancedPasteSettings _advancedPasteSettings; 44 private readonly AdvancedPasteAdditionalActions _additionalActions; 45 private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions; 46 private readonly DispatcherQueue _dispatcherQueue; 47 private IFileSystemWatcher _settingsWatcher; 48 private bool _suppressSave; 49 50 private GpoRuleConfigured _enabledGpoRuleConfiguration; 51 private bool _enabledStateIsGPOConfigured; 52 private GpoRuleConfigured _onlineAIModelsGpoRuleConfiguration; 53 private bool _onlineAIModelsDisallowedByGPO; 54 private bool _isEnabled; 55 56 private Func<string, int> SendConfigMSG { get; } 57 58 private static readonly HashSet<string> CustomActionNonPersistedProperties = new(StringComparer.Ordinal) 59 { 60 nameof(AdvancedPasteCustomAction.CanMoveUp), 61 nameof(AdvancedPasteCustomAction.CanMoveDown), 62 nameof(AdvancedPasteCustomAction.IsValid), 63 nameof(AdvancedPasteCustomAction.HasConflict), 64 nameof(AdvancedPasteCustomAction.Tooltip), 65 nameof(AdvancedPasteCustomAction.SubActions), 66 }; 67 68 public AdvancedPasteViewModel( 69 SettingsUtils settingsUtils, 70 ISettingsRepository<GeneralSettings> settingsRepository, 71 ISettingsRepository<AdvancedPasteSettings> advancedPasteSettingsRepository, 72 Func<string, int> ipcMSGCallBackFunc) 73 { 74 // To obtain the general settings configurations of PowerToys Settings. 75 ArgumentNullException.ThrowIfNull(settingsRepository); 76 77 GeneralSettingsConfig = settingsRepository.SettingsConfig; 78 79 // To obtain the settings configurations of Advanced Paste. 80 ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository); 81 82 _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); 83 84 _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); 85 86 _advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig ?? throw new ArgumentException("SettingsConfig cannot be null", nameof(advancedPasteSettingsRepository)); 87 88 if (_advancedPasteSettings.Properties is null) 89 { 90 throw new ArgumentException("AdvancedPasteSettings.Properties cannot be null", nameof(advancedPasteSettingsRepository)); 91 } 92 93 // Ensure AdditionalActions and CustomActions are initialized to prevent null reference exceptions 94 // This handles legacy settings files that may be missing these properties 95 _advancedPasteSettings.Properties.AdditionalActions ??= new AdvancedPasteAdditionalActions(); 96 _advancedPasteSettings.Properties.CustomActions ??= new AdvancedPasteCustomActions(); 97 98 AttachConfigurationHandlers(); 99 100 // set the callback functions value to handle outgoing IPC message. 101 SendConfigMSG = ipcMSGCallBackFunc; 102 103 _additionalActions = _advancedPasteSettings.Properties.AdditionalActions; 104 _customActions = _advancedPasteSettings.Properties.CustomActions.Value ?? new ObservableCollection<AdvancedPasteCustomAction>(); 105 106 SetupSettingsFileWatcher(); 107 108 InitializePasteAIProviderState(); 109 110 InitializeEnabledValue(); 111 MigrateLegacyAIEnablement(); 112 113 foreach (var action in _additionalActions.GetAllActions()) 114 { 115 action.PropertyChanged += OnAdditionalActionPropertyChanged; 116 } 117 118 foreach (var customAction in _customActions) 119 { 120 customAction.PropertyChanged += OnCustomActionPropertyChanged; 121 } 122 123 _customActions.CollectionChanged += OnCustomActionsCollectionChanged; 124 UpdateCustomActionsCanMoveUpDown(); 125 } 126 127 public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings() 128 { 129 var hotkeySettings = new List<HotkeySettings> 130 { 131 PasteAsPlainTextShortcut, 132 AdvancedPasteUIShortcut, 133 PasteAsMarkdownShortcut, 134 PasteAsJsonShortcut, 135 }; 136 137 foreach (var action in _additionalActions.GetAllActions()) 138 { 139 if (action is AdvancedPasteAdditionalAction additionalAction) 140 { 141 hotkeySettings.Add(additionalAction.Shortcut); 142 } 143 } 144 145 // Custom actions do not have localization header, just use the action name. 146 foreach (var customAction in _customActions) 147 { 148 hotkeySettings.Add(customAction.Shortcut); 149 } 150 151 return new Dictionary<string, HotkeySettings[]> 152 { 153 [ModuleName] = hotkeySettings.ToArray(), 154 }; 155 } 156 157 private void InitializeEnabledValue() 158 { 159 _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAdvancedPasteEnabledValue(); 160 if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) 161 { 162 // Get the enabled state from GPO. 163 _enabledStateIsGPOConfigured = true; 164 _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; 165 } 166 else 167 { 168 _isEnabled = GeneralSettingsConfig.Enabled.AdvancedPaste; 169 } 170 171 _onlineAIModelsGpoRuleConfiguration = GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue(); 172 _onlineAIModelsDisallowedByGPO = _onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled; 173 174 if (_onlineAIModelsDisallowedByGPO) 175 { 176 // disable AI if it was enabled 177 DisableAI(); 178 } 179 } 180 181 private void MigrateLegacyAIEnablement() 182 { 183 var properties = _advancedPasteSettings?.Properties; 184 if (properties is null) 185 { 186 return; 187 } 188 189 bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag); 190 bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag; 191 192 if (IsOnlineAIModelsDisallowedByGPO) 193 { 194 if (legacyAdvancedAIConsumed) 195 { 196 SaveAndNotifySettings(); 197 } 198 199 return; 200 } 201 202 PasswordCredential legacyCredential = TryGetLegacyOpenAICredential(); 203 204 if (legacyCredential is null) 205 { 206 if (legacyAdvancedAIConsumed) 207 { 208 SaveAndNotifySettings(); 209 } 210 211 return; 212 } 213 214 var configuration = properties.PasteAIConfiguration; 215 if (configuration is null) 216 { 217 configuration = new PasteAIConfiguration(); 218 properties.PasteAIConfiguration = configuration; 219 } 220 221 bool configurationUpdated = false; 222 223 var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration); 224 PasteAIProviderDefinition openAIProvider = ensureResult.Provider; 225 configurationUpdated |= ensureResult.Updated; 226 227 if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled) 228 { 229 openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled; 230 configurationUpdated = true; 231 } 232 233 if (legacyCredential is not null && openAIProvider is not null) 234 { 235 SavePasteAIApiKey(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password); 236 RemoveLegacyOpenAICredential(); 237 } 238 239 const bool shouldEnableAI = true; 240 bool enabledChanged = false; 241 if (properties.IsAIEnabled != shouldEnableAI) 242 { 243 properties.IsAIEnabled = shouldEnableAI; 244 enabledChanged = true; 245 } 246 247 bool shouldPersist = configurationUpdated || enabledChanged || legacyAdvancedAIConsumed; 248 249 if (shouldPersist) 250 { 251 SaveAndNotifySettings(); 252 253 if (configurationUpdated) 254 { 255 OnPropertyChanged(nameof(PasteAIConfiguration)); 256 } 257 258 if (enabledChanged) 259 { 260 OnPropertyChanged(nameof(IsAIEnabled)); 261 } 262 } 263 } 264 265 public bool IsEnabled 266 { 267 get => _isEnabled; 268 set 269 { 270 if (_enabledStateIsGPOConfigured) 271 { 272 // If it's GPO configured, shouldn't be able to change this state. 273 return; 274 } 275 276 if (_isEnabled != value) 277 { 278 _isEnabled = value; 279 OnPropertyChanged(nameof(IsEnabled)); 280 OnPropertyChanged(nameof(ShowOnlineAIModelsGpoConfiguredInfoBar)); 281 OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar)); 282 283 // Set the status of AdvancedPaste in the general settings 284 GeneralSettingsConfig.Enabled.AdvancedPaste = value; 285 var outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); 286 287 SendConfigMSG(outgoing.ToString()); 288 } 289 } 290 } 291 292 public ObservableCollection<AdvancedPasteCustomAction> CustomActions => _customActions; 293 294 public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions; 295 296 public static IEnumerable<AIServiceTypeMetadata> AvailableProviders => AIServiceTypeRegistry.GetAvailableServiceTypes(); 297 298 /// <summary> 299 /// Gets available AI providers filtered by GPO policies. 300 /// Only returns providers that are not explicitly disabled by GPO. 301 /// </summary> 302 public IEnumerable<AIServiceTypeMetadata> AvailableProvidersFilteredByGPO => 303 AvailableProviders.Where(metadata => IsServiceTypeAllowedByGPO(metadata.ServiceType)); 304 305 public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO; 306 307 private PasswordCredential TryGetLegacyOpenAICredential() 308 { 309 try 310 { 311 PasswordVault vault = new(); 312 var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); 313 credential?.RetrievePassword(); 314 return credential; 315 } 316 catch (Exception) 317 { 318 return null; 319 } 320 } 321 322 private void RemoveLegacyOpenAICredential() 323 { 324 try 325 { 326 PasswordVault vault = new(); 327 TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); 328 } 329 catch (Exception) 330 { 331 } 332 } 333 334 public bool IsEnabledGpoConfigured 335 { 336 get => _enabledStateIsGPOConfigured; 337 } 338 339 public bool IsOnlineAIModelsDisallowedByGPO 340 { 341 get => _onlineAIModelsDisallowedByGPO || _enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled; 342 } 343 344 public bool ShowOnlineAIModelsGpoConfiguredInfoBar 345 { 346 get => _onlineAIModelsDisallowedByGPO && _isEnabled; 347 } 348 349 private bool IsClipboardHistoryEnabled() 350 { 351 string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\"; 352 try 353 { 354 int enableClipboardHistory = (int)Registry.GetValue(registryKey, "EnableClipboardHistory", false); 355 return enableClipboardHistory != 0; 356 } 357 catch (Exception) 358 { 359 return false; 360 } 361 } 362 363 private bool IsClipboardHistoryDisabledByGPO() 364 { 365 string registryKey = @"HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System\"; 366 try 367 { 368 object allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null); 369 if (allowClipboardHistory != null) 370 { 371 return (int)allowClipboardHistory == 0; 372 } 373 else 374 { 375 return false; 376 } 377 } 378 catch (Exception) 379 { 380 return false; 381 } 382 } 383 384 private void SetClipboardHistoryEnabled(bool value) 385 { 386 string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\"; 387 try 388 { 389 Registry.SetValue(registryKey, "EnableClipboardHistory", value ? 1 : 0); 390 } 391 catch (Exception) 392 { 393 } 394 } 395 396 public bool ClipboardHistoryEnabled 397 { 398 get => IsClipboardHistoryEnabled(); 399 set 400 { 401 if (IsClipboardHistoryEnabled() != value) 402 { 403 SetClipboardHistoryEnabled(value); 404 } 405 } 406 } 407 408 public bool ClipboardHistoryDisabledByGPO 409 { 410 get => IsClipboardHistoryDisabledByGPO(); 411 } 412 413 public bool ShowClipboardHistoryIsGpoConfiguredInfoBar 414 { 415 get => IsClipboardHistoryDisabledByGPO() && _isEnabled; 416 } 417 418 public HotkeySettings AdvancedPasteUIShortcut 419 { 420 get => _advancedPasteSettings.Properties.AdvancedPasteUIShortcut; 421 set 422 { 423 if (_advancedPasteSettings.Properties.AdvancedPasteUIShortcut != value) 424 { 425 _advancedPasteSettings.Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut; 426 OnPropertyChanged(nameof(IsConflictingCopyShortcut)); 427 OnPropertyChanged(nameof(AdvancedPasteUIShortcut)); 428 SaveAndNotifySettings(); 429 } 430 } 431 } 432 433 public HotkeySettings PasteAsPlainTextShortcut 434 { 435 get => _advancedPasteSettings.Properties.PasteAsPlainTextShortcut; 436 set 437 { 438 if (_advancedPasteSettings.Properties.PasteAsPlainTextShortcut != value) 439 { 440 _advancedPasteSettings.Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut; 441 OnPropertyChanged(nameof(IsConflictingCopyShortcut)); 442 OnPropertyChanged(nameof(PasteAsPlainTextShortcut)); 443 SaveAndNotifySettings(); 444 } 445 } 446 } 447 448 public HotkeySettings PasteAsMarkdownShortcut 449 { 450 get => _advancedPasteSettings.Properties.PasteAsMarkdownShortcut; 451 set 452 { 453 if (_advancedPasteSettings.Properties.PasteAsMarkdownShortcut != value) 454 { 455 _advancedPasteSettings.Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings(); 456 OnPropertyChanged(nameof(IsConflictingCopyShortcut)); 457 OnPropertyChanged(nameof(PasteAsMarkdownShortcut)); 458 SaveAndNotifySettings(); 459 } 460 } 461 } 462 463 public HotkeySettings PasteAsJsonShortcut 464 { 465 get => _advancedPasteSettings.Properties.PasteAsJsonShortcut; 466 set 467 { 468 if (_advancedPasteSettings.Properties.PasteAsJsonShortcut != value) 469 { 470 _advancedPasteSettings.Properties.PasteAsJsonShortcut = value ?? new HotkeySettings(); 471 OnPropertyChanged(nameof(IsConflictingCopyShortcut)); 472 OnPropertyChanged(nameof(PasteAsJsonShortcut)); 473 SaveAndNotifySettings(); 474 } 475 } 476 } 477 478 public PasteAIConfiguration PasteAIConfiguration 479 { 480 get 481 { 482 // Ensure PasteAIConfiguration is never null for XAML binding 483 _advancedPasteSettings.Properties.PasteAIConfiguration ??= new PasteAIConfiguration(); 484 return _advancedPasteSettings.Properties.PasteAIConfiguration; 485 } 486 487 set 488 { 489 if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration)) 490 { 491 UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration); 492 493 var newValue = value ?? new PasteAIConfiguration(); 494 _advancedPasteSettings.Properties.PasteAIConfiguration = newValue; 495 SubscribeToPasteAIConfiguration(newValue); 496 497 OnPropertyChanged(nameof(PasteAIConfiguration)); 498 SaveAndNotifySettings(); 499 } 500 } 501 } 502 503 public PasteAIProviderDefinition PasteAIProviderDraft 504 { 505 get => _pasteAIProviderDraft; 506 private set 507 { 508 if (!ReferenceEquals(_pasteAIProviderDraft, value)) 509 { 510 _pasteAIProviderDraft = value; 511 OnPropertyChanged(nameof(PasteAIProviderDraft)); 512 OnPropertyChanged(nameof(ShowPasteAIProviderGpoConfiguredInfoBar)); 513 } 514 } 515 } 516 517 public bool ShowPasteAIProviderGpoConfiguredInfoBar 518 { 519 get 520 { 521 if (_pasteAIProviderDraft is null) 522 { 523 return false; 524 } 525 526 var serviceType = _pasteAIProviderDraft.ServiceType.ToAIServiceType(); 527 return !IsServiceTypeAllowedByGPO(serviceType); 528 } 529 } 530 531 public bool IsEditingPasteAIProvider => _editingPasteAIProvider is not null; 532 533 public bool ShowCustomPreview 534 { 535 get => _advancedPasteSettings.Properties.ShowCustomPreview; 536 set 537 { 538 if (value != _advancedPasteSettings.Properties.ShowCustomPreview) 539 { 540 _advancedPasteSettings.Properties.ShowCustomPreview = value; 541 NotifySettingsChanged(); 542 } 543 } 544 } 545 546 public bool CloseAfterLosingFocus 547 { 548 get => _advancedPasteSettings.Properties.CloseAfterLosingFocus; 549 set 550 { 551 if (value != _advancedPasteSettings.Properties.CloseAfterLosingFocus) 552 { 553 _advancedPasteSettings.Properties.CloseAfterLosingFocus = value; 554 NotifySettingsChanged(); 555 } 556 } 557 } 558 559 public bool EnableClipboardPreview 560 { 561 get => _advancedPasteSettings.Properties.EnableClipboardPreview; 562 set 563 { 564 if (value != _advancedPasteSettings.Properties.EnableClipboardPreview) 565 { 566 _advancedPasteSettings.Properties.EnableClipboardPreview = value; 567 NotifySettingsChanged(); 568 } 569 } 570 } 571 572 public bool AutoCopySelectionForCustomActionHotkey 573 { 574 get => _advancedPasteSettings.Properties.AutoCopySelectionForCustomActionHotkey; 575 set 576 { 577 if (value != _advancedPasteSettings.Properties.AutoCopySelectionForCustomActionHotkey) 578 { 579 _advancedPasteSettings.Properties.AutoCopySelectionForCustomActionHotkey = value; 580 NotifySettingsChanged(); 581 } 582 } 583 } 584 585 public bool IsConflictingCopyShortcut => 586 _customActions.Select(customAction => customAction.Shortcut) 587 .Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut]) 588 .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString())); 589 590 public bool IsAdditionalActionConflictingCopyShortcut => 591 _additionalActions.GetAllActions() 592 .OfType<AdvancedPasteAdditionalAction>() 593 .Select(additionalAction => additionalAction.Shortcut) 594 .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString())); 595 596 private void NotifySettingsChanged() 597 { 598 // Using InvariantCulture as this is an IPC message 599 SendConfigMSG( 600 string.Format( 601 CultureInfo.InvariantCulture, 602 "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", 603 AdvancedPasteSettings.ModuleName, 604 JsonSerializer.Serialize(_advancedPasteSettings, SourceGenerationContextContext.Default.AdvancedPasteSettings))); 605 } 606 607 public void RefreshEnabledState() 608 { 609 InitializeEnabledValue(); 610 MigrateLegacyAIEnablement(); 611 OnPropertyChanged(nameof(IsEnabled)); 612 OnPropertyChanged(nameof(ShowOnlineAIModelsGpoConfiguredInfoBar)); 613 OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar)); 614 OnPropertyChanged(nameof(IsAIEnabled)); 615 } 616 617 public void BeginAddPasteAIProvider(string serviceType) 618 { 619 var normalizedServiceType = NormalizeServiceType(serviceType, out var persistedServiceType); 620 621 var metadata = AIServiceTypeRegistry.GetMetadata(normalizedServiceType); 622 var provider = new PasteAIProviderDefinition 623 { 624 ServiceType = persistedServiceType, 625 ModelName = PasteAIProviderDefaults.GetDefaultModelName(normalizedServiceType), 626 EndpointUrl = string.Empty, 627 ApiVersion = string.Empty, 628 DeploymentName = string.Empty, 629 ModelPath = string.Empty, 630 SystemPrompt = string.Empty, 631 ModerationEnabled = normalizedServiceType == AIServiceType.OpenAI, 632 IsLocalModel = metadata.IsLocalModel, 633 }; 634 635 if (normalizedServiceType is AIServiceType.FoundryLocal or AIServiceType.Onnx or AIServiceType.ML) 636 { 637 provider.ModelName = string.Empty; 638 } 639 640 _editingPasteAIProvider = null; 641 PasteAIProviderDraft = provider; 642 } 643 644 private static AIServiceType NormalizeServiceType(string serviceType, out string persistedServiceType) 645 { 646 if (string.IsNullOrWhiteSpace(serviceType)) 647 { 648 persistedServiceType = AIServiceType.OpenAI.ToConfigurationString(); 649 return AIServiceType.OpenAI; 650 } 651 652 var trimmed = serviceType.Trim(); 653 var serviceTypeKind = trimmed.ToAIServiceType(); 654 655 if (serviceTypeKind == AIServiceType.Unknown) 656 { 657 persistedServiceType = AIServiceType.OpenAI.ToConfigurationString(); 658 return AIServiceType.OpenAI; 659 } 660 661 persistedServiceType = trimmed; 662 return serviceTypeKind; 663 } 664 665 public bool IsServiceTypeAllowedByGPO(AIServiceType serviceType) 666 { 667 var metadata = AIServiceTypeRegistry.GetMetadata(serviceType); 668 669 // Check if this is an online service 670 if (metadata.IsOnlineService) 671 { 672 // For online services, first check the global online AI models GPO 673 if (_onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled) 674 { 675 // If global online AI is disabled, all online services are blocked 676 return false; 677 } 678 679 // If global online AI is enabled or not configured, check individual endpoint GPO 680 var individualGpoRule = serviceType switch 681 { 682 AIServiceType.OpenAI => GPOWrapper.GetAllowedAdvancedPasteOpenAIValue(), 683 AIServiceType.AzureOpenAI => GPOWrapper.GetAllowedAdvancedPasteAzureOpenAIValue(), 684 AIServiceType.AzureAIInference => GPOWrapper.GetAllowedAdvancedPasteAzureAIInferenceValue(), 685 AIServiceType.Mistral => GPOWrapper.GetAllowedAdvancedPasteMistralValue(), 686 AIServiceType.Google => GPOWrapper.GetAllowedAdvancedPasteGoogleValue(), 687 _ => GpoRuleConfigured.Unavailable, 688 }; 689 690 // If individual GPO is explicitly disabled, block it 691 return individualGpoRule != GpoRuleConfigured.Disabled; 692 } 693 else 694 { 695 // For local models, only check their individual GPO (not affected by online AI GPO) 696 var localGpoRule = serviceType switch 697 { 698 AIServiceType.Ollama => GPOWrapper.GetAllowedAdvancedPasteOllamaValue(), 699 AIServiceType.FoundryLocal => GPOWrapper.GetAllowedAdvancedPasteFoundryLocalValue(), 700 _ => GpoRuleConfigured.Unavailable, 701 }; 702 703 // If local model GPO is explicitly disabled, block it 704 return localGpoRule != GpoRuleConfigured.Disabled; 705 } 706 } 707 708 public void BeginEditPasteAIProvider(PasteAIProviderDefinition provider) 709 { 710 ArgumentNullException.ThrowIfNull(provider); 711 712 _editingPasteAIProvider = provider; 713 var draft = provider.Clone(); 714 var storedEndpoint = GetPasteAIEndpoint(draft.Id, draft.ServiceType); 715 if (!string.IsNullOrWhiteSpace(storedEndpoint)) 716 { 717 draft.EndpointUrl = storedEndpoint; 718 } 719 720 PasteAIProviderDraft = draft; 721 } 722 723 public void CancelPasteAIProviderDraft() 724 { 725 PasteAIProviderDraft = null; 726 _editingPasteAIProvider = null; 727 } 728 729 public void CommitPasteAIProviderDraft(string apiKey, string endpoint) 730 { 731 if (PasteAIProviderDraft is null) 732 { 733 return; 734 } 735 736 var config = PasteAIConfiguration ?? new PasteAIConfiguration(); 737 if (_advancedPasteSettings.Properties.PasteAIConfiguration is null) 738 { 739 PasteAIConfiguration = config; 740 } 741 742 var draft = PasteAIProviderDraft; 743 draft.EndpointUrl = endpoint?.Trim() ?? string.Empty; 744 745 SavePasteAIApiKey(draft.Id, draft.ServiceType, apiKey); 746 747 if (_editingPasteAIProvider is null) 748 { 749 config.Providers.Add(draft); 750 config.ActiveProviderId ??= draft.Id; 751 } 752 else 753 { 754 UpdateProviderFromDraft(_editingPasteAIProvider, draft); 755 _editingPasteAIProvider = null; 756 } 757 758 PasteAIProviderDraft = null; 759 SaveAndNotifySettings(); 760 OnPropertyChanged(nameof(PasteAIConfiguration)); 761 } 762 763 public void RemovePasteAIProvider(PasteAIProviderDefinition provider) 764 { 765 if (provider is null) 766 { 767 return; 768 } 769 770 var config = PasteAIConfiguration; 771 if (config?.Providers is null) 772 { 773 return; 774 } 775 776 if (config.Providers.Remove(provider)) 777 { 778 RemovePasteAICredentials(provider.Id, provider.ServiceType); 779 SaveAndNotifySettings(); 780 OnPropertyChanged(nameof(PasteAIConfiguration)); 781 } 782 } 783 784 protected override void Dispose(bool disposing) 785 { 786 if (!_disposed) 787 { 788 if (disposing) 789 { 790 UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings?.Properties.PasteAIConfiguration); 791 792 foreach (var action in _additionalActions.GetAllActions()) 793 { 794 action.PropertyChanged -= OnAdditionalActionPropertyChanged; 795 } 796 797 foreach (var customAction in _customActions) 798 { 799 customAction.PropertyChanged -= OnCustomActionPropertyChanged; 800 } 801 802 _customActions.CollectionChanged -= OnCustomActionsCollectionChanged; 803 _settingsWatcher?.Dispose(); 804 _settingsWatcher = null; 805 } 806 807 _disposed = true; 808 } 809 810 base.Dispose(disposing); 811 } 812 813 internal void DisableAI() 814 { 815 try 816 { 817 bool stateChanged = false; 818 819 if (_advancedPasteSettings.Properties.IsAIEnabled) 820 { 821 _advancedPasteSettings.Properties.IsAIEnabled = false; 822 stateChanged = true; 823 } 824 825 if (stateChanged) 826 { 827 SaveAndNotifySettings(); 828 } 829 else 830 { 831 NotifySettingsChanged(); 832 } 833 834 OnPropertyChanged(nameof(IsAIEnabled)); 835 } 836 catch (Exception) 837 { 838 } 839 } 840 841 internal void EnableAI() 842 { 843 try 844 { 845 if (IsOnlineAIModelsDisallowedByGPO) 846 { 847 return; 848 } 849 850 bool stateChanged = false; 851 852 if (!_advancedPasteSettings.Properties.IsAIEnabled) 853 { 854 _advancedPasteSettings.Properties.IsAIEnabled = true; 855 stateChanged = true; 856 } 857 858 if (stateChanged) 859 { 860 SaveAndNotifySettings(); 861 } 862 else 863 { 864 NotifySettingsChanged(); 865 } 866 867 OnPropertyChanged(nameof(IsAIEnabled)); 868 } 869 catch (Exception) 870 { 871 } 872 } 873 874 internal void SavePasteAIApiKey(string providerId, string serviceType, string apiKey) 875 { 876 try 877 { 878 apiKey = apiKey?.Trim() ?? string.Empty; 879 serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType; 880 providerId ??= string.Empty; 881 882 string credentialResource = GetAICredentialResource(serviceType); 883 string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType); 884 string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(providerId, serviceType); 885 PasswordVault vault = new(); 886 TryRemoveCredential(vault, credentialResource, credentialUserName); 887 TryRemoveCredential(vault, credentialResource, endpointCredentialUserName); 888 889 bool storeApiKey = RequiresCredentialStorage(serviceType) && !string.IsNullOrWhiteSpace(apiKey); 890 if (storeApiKey) 891 { 892 PasswordCredential cred = new(credentialResource, credentialUserName, apiKey); 893 vault.Add(cred); 894 } 895 896 OnPropertyChanged(nameof(IsAIEnabled)); 897 NotifySettingsChanged(); 898 } 899 catch (Exception) 900 { 901 } 902 } 903 904 internal string GetPasteAIApiKey(string providerId, string serviceType) 905 { 906 serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType; 907 providerId ??= string.Empty; 908 return RetrieveCredentialValue( 909 GetAICredentialResource(serviceType), 910 GetPasteAICredentialUserName(providerId, serviceType)); 911 } 912 913 internal string GetPasteAIEndpoint(string providerId, string serviceType) 914 { 915 providerId ??= string.Empty; 916 var providers = PasteAIConfiguration?.Providers; 917 if (providers is null) 918 { 919 return string.Empty; 920 } 921 922 var provider = providers.FirstOrDefault(p => string.Equals(p.Id ?? string.Empty, providerId, StringComparison.OrdinalIgnoreCase)); 923 if (provider is null && !string.IsNullOrWhiteSpace(serviceType)) 924 { 925 provider = providers.FirstOrDefault(p => string.Equals(p.ServiceType, serviceType, StringComparison.OrdinalIgnoreCase)); 926 } 927 928 return provider?.EndpointUrl?.Trim() ?? string.Empty; 929 } 930 931 private string GetAICredentialResource(string serviceType) 932 { 933 serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType; 934 return serviceType.ToLowerInvariant() switch 935 { 936 "openai" => "https://platform.openai.com/api-keys", 937 "azureopenai" => "https://azure.microsoft.com/products/ai-services/openai-service", 938 "azureaiinference" => "https://azure.microsoft.com/products/ai-services/ai-inference", 939 "mistral" => "https://console.mistral.ai/account/api-keys", 940 "google" => "https://ai.google.dev/", 941 "ollama" => "https://ollama.com/", 942 _ => "https://platform.openai.com/api-keys", 943 }; 944 } 945 946 private string GetPasteAICredentialUserName(string providerId, string serviceType) 947 { 948 serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType; 949 providerId ??= string.Empty; 950 951 string service = serviceType.ToLowerInvariant(); 952 string normalizedId = NormalizeProviderIdentifier(providerId); 953 954 return $"PowerToys_AdvancedPaste_PasteAI_{service}_{normalizedId}"; 955 } 956 957 private string GetPasteAIEndpointCredentialUserName(string providerId, string serviceType) 958 { 959 return GetPasteAICredentialUserName(providerId, serviceType) + "_Endpoint"; 960 } 961 962 private static void UpdateProviderFromDraft(PasteAIProviderDefinition target, PasteAIProviderDefinition source) 963 { 964 if (target is null || source is null) 965 { 966 return; 967 } 968 969 target.ServiceType = source.ServiceType; 970 target.ModelName = source.ModelName; 971 target.EndpointUrl = source.EndpointUrl; 972 target.ApiVersion = source.ApiVersion; 973 target.DeploymentName = source.DeploymentName; 974 target.ModelPath = source.ModelPath; 975 target.SystemPrompt = source.SystemPrompt; 976 target.ModerationEnabled = source.ModerationEnabled; 977 target.EnableAdvancedAI = source.EnableAdvancedAI; 978 target.IsLocalModel = source.IsLocalModel; 979 } 980 981 private void RemovePasteAICredentials(string providerId, string serviceType) 982 { 983 try 984 { 985 serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType; 986 providerId ??= string.Empty; 987 988 string credentialResource = GetAICredentialResource(serviceType); 989 PasswordVault vault = new(); 990 TryRemoveCredential(vault, credentialResource, GetPasteAICredentialUserName(providerId, serviceType)); 991 TryRemoveCredential(vault, credentialResource, GetPasteAIEndpointCredentialUserName(providerId, serviceType)); 992 } 993 catch (Exception) 994 { 995 } 996 } 997 998 private static string NormalizeProviderIdentifier(string providerId) 999 { 1000 if (string.IsNullOrWhiteSpace(providerId)) 1001 { 1002 return "default"; 1003 } 1004 1005 var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray()); 1006 return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant(); 1007 } 1008 1009 private static bool RequiresCredentialStorage(string serviceType) 1010 { 1011 var serviceTypeKind = serviceType.ToAIServiceType(); 1012 1013 return serviceTypeKind switch 1014 { 1015 AIServiceType.Onnx => false, 1016 AIServiceType.Ollama => false, 1017 AIServiceType.FoundryLocal => false, 1018 AIServiceType.ML => false, 1019 _ => true, 1020 }; 1021 } 1022 1023 private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName) 1024 { 1025 try 1026 { 1027 PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName); 1028 vault.Remove(existingCred); 1029 } 1030 catch (Exception) 1031 { 1032 // Credential doesn't exist, which is fine 1033 } 1034 } 1035 1036 internal AdvancedPasteCustomAction GetNewCustomAction(string namePrefix) 1037 { 1038 ArgumentException.ThrowIfNullOrEmpty(namePrefix); 1039 1040 var maxUsedPrefix = _customActions.Select(customAction => customAction.Name) 1041 .Where(name => name.StartsWith(namePrefix, StringComparison.InvariantCulture)) 1042 .Select(name => int.TryParse(name.AsSpan(namePrefix.Length), out int number) ? number : 0) 1043 .DefaultIfEmpty(0) 1044 .Max(); 1045 1046 var maxUsedId = _customActions.Select(customAction => customAction.Id) 1047 .DefaultIfEmpty(-1) 1048 .Max(); 1049 return new() 1050 { 1051 Id = maxUsedId + 1, 1052 Name = $"{namePrefix} {maxUsedPrefix + 1}", 1053 IsShown = true, 1054 }; 1055 } 1056 1057 internal void AddCustomAction(AdvancedPasteCustomAction customAction) 1058 { 1059 if (_customActions.Any(existingCustomAction => existingCustomAction.Id == customAction.Id)) 1060 { 1061 throw new ArgumentException("Duplicate custom action", nameof(customAction)); 1062 } 1063 1064 _customActions.Add(customAction); 1065 } 1066 1067 internal void DeleteCustomAction(AdvancedPasteCustomAction customAction) => _customActions.Remove(customAction); 1068 1069 private void SaveCustomActions() => SaveAndNotifySettings(); 1070 1071 private void SaveAndNotifySettings() 1072 { 1073 if (_suppressSave) 1074 { 1075 return; 1076 } 1077 1078 _settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName); 1079 NotifySettingsChanged(); 1080 } 1081 1082 private void OnAdditionalActionPropertyChanged(object sender, PropertyChangedEventArgs e) 1083 { 1084 SaveAndNotifySettings(); 1085 1086 if (e.PropertyName == nameof(AdvancedPasteAdditionalAction.Shortcut)) 1087 { 1088 OnPropertyChanged(nameof(IsAdditionalActionConflictingCopyShortcut)); 1089 } 1090 } 1091 1092 private void OnCustomActionPropertyChanged(object sender, PropertyChangedEventArgs e) 1093 { 1094 if (!string.IsNullOrEmpty(e.PropertyName) && !CustomActionNonPersistedProperties.Contains(e.PropertyName)) 1095 { 1096 SaveCustomActions(); 1097 } 1098 1099 if (e.PropertyName == nameof(AdvancedPasteCustomAction.Shortcut)) 1100 { 1101 OnPropertyChanged(nameof(IsConflictingCopyShortcut)); 1102 } 1103 } 1104 1105 private void OnCustomActionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 1106 { 1107 void AddRange(System.Collections.IList items) 1108 { 1109 foreach (AdvancedPasteCustomAction item in items) 1110 { 1111 item.PropertyChanged += OnCustomActionPropertyChanged; 1112 } 1113 } 1114 1115 void RemoveRange(System.Collections.IList items) 1116 { 1117 foreach (AdvancedPasteCustomAction item in items) 1118 { 1119 item.PropertyChanged -= OnCustomActionPropertyChanged; 1120 } 1121 } 1122 1123 switch (e.Action) 1124 { 1125 case NotifyCollectionChangedAction.Add: 1126 AddRange(e.NewItems); 1127 break; 1128 1129 case NotifyCollectionChangedAction.Remove: 1130 RemoveRange(e.OldItems); 1131 break; 1132 1133 case NotifyCollectionChangedAction.Replace: 1134 AddRange(e.NewItems); 1135 RemoveRange(e.OldItems); 1136 break; 1137 1138 case NotifyCollectionChangedAction.Move: 1139 break; 1140 1141 default: 1142 throw new ArgumentException($"Unsupported {nameof(e.Action)} {e.Action}", nameof(e)); 1143 } 1144 1145 OnPropertyChanged(nameof(IsConflictingCopyShortcut)); 1146 UpdateCustomActionsCanMoveUpDown(); 1147 SaveCustomActions(); 1148 } 1149 1150 private void AttachConfigurationHandlers() 1151 { 1152 SubscribeToPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration); 1153 } 1154 1155 private void SetupSettingsFileWatcher() 1156 { 1157 _settingsWatcher = Helper.GetFileWatcher(AdvancedPasteSettings.ModuleName, SettingsUtils.DefaultFileName, OnSettingsFileChanged); 1158 } 1159 1160 private void OnSettingsFileChanged() 1161 { 1162 if (_disposed) 1163 { 1164 return; 1165 } 1166 1167 void Handler() 1168 { 1169 ApplyExternalSettings(); 1170 } 1171 1172 if (_dispatcherQueue is not null && !_dispatcherQueue.HasThreadAccess) 1173 { 1174 _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, Handler); 1175 } 1176 else 1177 { 1178 Handler(); 1179 } 1180 } 1181 1182 private void ApplyExternalSettings() 1183 { 1184 if (_disposed) 1185 { 1186 return; 1187 } 1188 1189 AdvancedPasteSettings latestSettings; 1190 1191 try 1192 { 1193 latestSettings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteSettings.ModuleName); 1194 } 1195 catch 1196 { 1197 return; 1198 } 1199 1200 if (latestSettings?.Properties is null) 1201 { 1202 return; 1203 } 1204 1205 try 1206 { 1207 _suppressSave = true; 1208 ApplyExternalProperties(latestSettings.Properties); 1209 } 1210 finally 1211 { 1212 _suppressSave = false; 1213 } 1214 } 1215 1216 private void ApplyExternalProperties(AdvancedPasteProperties source) 1217 { 1218 var target = _advancedPasteSettings?.Properties; 1219 1220 if (target is null || source is null) 1221 { 1222 return; 1223 } 1224 1225 if (target.IsAIEnabled != source.IsAIEnabled) 1226 { 1227 target.IsAIEnabled = source.IsAIEnabled; 1228 OnPropertyChanged(nameof(IsAIEnabled)); 1229 } 1230 1231 if (target.ShowCustomPreview != source.ShowCustomPreview) 1232 { 1233 target.ShowCustomPreview = source.ShowCustomPreview; 1234 OnPropertyChanged(nameof(ShowCustomPreview)); 1235 } 1236 1237 if (target.CloseAfterLosingFocus != source.CloseAfterLosingFocus) 1238 { 1239 target.CloseAfterLosingFocus = source.CloseAfterLosingFocus; 1240 OnPropertyChanged(nameof(CloseAfterLosingFocus)); 1241 } 1242 1243 if (target.EnableClipboardPreview != source.EnableClipboardPreview) 1244 { 1245 target.EnableClipboardPreview = source.EnableClipboardPreview; 1246 OnPropertyChanged(nameof(EnableClipboardPreview)); 1247 } 1248 1249 if (target.AutoCopySelectionForCustomActionHotkey != source.AutoCopySelectionForCustomActionHotkey) 1250 { 1251 target.AutoCopySelectionForCustomActionHotkey = source.AutoCopySelectionForCustomActionHotkey; 1252 OnPropertyChanged(nameof(AutoCopySelectionForCustomActionHotkey)); 1253 } 1254 1255 var incomingConfig = source.PasteAIConfiguration ?? new PasteAIConfiguration(); 1256 if (ShouldReplacePasteAIConfiguration(target.PasteAIConfiguration, incomingConfig)) 1257 { 1258 PasteAIConfiguration = incomingConfig; 1259 } 1260 } 1261 1262 private static bool ShouldReplacePasteAIConfiguration(PasteAIConfiguration current, PasteAIConfiguration incoming) 1263 { 1264 if (incoming is null) 1265 { 1266 return false; 1267 } 1268 1269 if (current is null) 1270 { 1271 return true; 1272 } 1273 1274 if (!string.Equals(current.ActiveProviderId ?? string.Empty, incoming.ActiveProviderId ?? string.Empty, StringComparison.OrdinalIgnoreCase)) 1275 { 1276 return true; 1277 } 1278 1279 var currentProviders = current.Providers ?? new ObservableCollection<PasteAIProviderDefinition>(); 1280 var incomingProviders = incoming.Providers ?? new ObservableCollection<PasteAIProviderDefinition>(); 1281 1282 if (currentProviders.Count != incomingProviders.Count) 1283 { 1284 return true; 1285 } 1286 1287 for (int i = 0; i < currentProviders.Count; i++) 1288 { 1289 var existing = currentProviders[i]; 1290 var updated = incomingProviders[i]; 1291 1292 if (!string.Equals(existing?.Id ?? string.Empty, updated?.Id ?? string.Empty, StringComparison.OrdinalIgnoreCase)) 1293 { 1294 return true; 1295 } 1296 1297 if (!string.Equals(existing?.ServiceType ?? string.Empty, updated?.ServiceType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) 1298 { 1299 return true; 1300 } 1301 1302 if (!string.Equals(existing?.ModelName ?? string.Empty, updated?.ModelName ?? string.Empty, StringComparison.Ordinal)) 1303 { 1304 return true; 1305 } 1306 1307 if (!string.Equals(existing?.EndpointUrl ?? string.Empty, updated?.EndpointUrl ?? string.Empty, StringComparison.Ordinal)) 1308 { 1309 return true; 1310 } 1311 1312 if (!string.Equals(existing?.DeploymentName ?? string.Empty, updated?.DeploymentName ?? string.Empty, StringComparison.Ordinal)) 1313 { 1314 return true; 1315 } 1316 1317 if (!string.Equals(existing?.ApiVersion ?? string.Empty, updated?.ApiVersion ?? string.Empty, StringComparison.Ordinal)) 1318 { 1319 return true; 1320 } 1321 1322 if (!string.Equals(existing?.SystemPrompt ?? string.Empty, updated?.SystemPrompt ?? string.Empty, StringComparison.Ordinal)) 1323 { 1324 return true; 1325 } 1326 1327 if (existing?.ModerationEnabled != updated?.ModerationEnabled || existing?.EnableAdvancedAI != updated?.EnableAdvancedAI || existing?.IsActive != updated?.IsActive) 1328 { 1329 return true; 1330 } 1331 } 1332 1333 return false; 1334 } 1335 1336 private void SubscribeToPasteAIConfiguration(PasteAIConfiguration configuration) 1337 { 1338 if (configuration is not null) 1339 { 1340 configuration.PropertyChanged += OnPasteAIConfigurationPropertyChanged; 1341 SubscribeToPasteAIProviders(configuration); 1342 } 1343 } 1344 1345 private void UnsubscribeFromPasteAIConfiguration(PasteAIConfiguration configuration) 1346 { 1347 if (configuration is not null) 1348 { 1349 configuration.PropertyChanged -= OnPasteAIConfigurationPropertyChanged; 1350 UnsubscribeFromPasteAIProviders(configuration); 1351 } 1352 } 1353 1354 private void SubscribeToPasteAIProviders(PasteAIConfiguration configuration) 1355 { 1356 if (configuration?.Providers is null) 1357 { 1358 return; 1359 } 1360 1361 configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged; 1362 configuration.Providers.CollectionChanged += OnPasteAIProvidersCollectionChanged; 1363 1364 foreach (var provider in configuration.Providers) 1365 { 1366 provider.PropertyChanged -= OnPasteAIProviderPropertyChanged; 1367 provider.PropertyChanged += OnPasteAIProviderPropertyChanged; 1368 } 1369 } 1370 1371 private void UnsubscribeFromPasteAIProviders(PasteAIConfiguration configuration) 1372 { 1373 if (configuration?.Providers is null) 1374 { 1375 return; 1376 } 1377 1378 configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged; 1379 1380 foreach (var provider in configuration.Providers) 1381 { 1382 provider.PropertyChanged -= OnPasteAIProviderPropertyChanged; 1383 } 1384 } 1385 1386 private void OnPasteAIProvidersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 1387 { 1388 if (e?.NewItems is not null) 1389 { 1390 foreach (PasteAIProviderDefinition provider in e.NewItems) 1391 { 1392 provider.PropertyChanged += OnPasteAIProviderPropertyChanged; 1393 } 1394 } 1395 1396 if (e?.OldItems is not null) 1397 { 1398 foreach (PasteAIProviderDefinition provider in e.OldItems) 1399 { 1400 provider.PropertyChanged -= OnPasteAIProviderPropertyChanged; 1401 } 1402 } 1403 1404 var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration; 1405 1406 OnPropertyChanged(nameof(PasteAIConfiguration)); 1407 SaveAndNotifySettings(); 1408 } 1409 1410 private void OnPasteAIProviderPropertyChanged(object sender, PropertyChangedEventArgs e) 1411 { 1412 if (sender is PasteAIProviderDefinition provider) 1413 { 1414 // When service type changes we may need to update credentials entry names. 1415 if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.ServiceType), StringComparison.Ordinal)) 1416 { 1417 SaveAndNotifySettings(); 1418 return; 1419 } 1420 1421 SaveAndNotifySettings(); 1422 } 1423 } 1424 1425 private void OnPasteAIConfigurationPropertyChanged(object sender, PropertyChangedEventArgs e) 1426 { 1427 if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.Providers), StringComparison.Ordinal)) 1428 { 1429 SubscribeToPasteAIProviders(PasteAIConfiguration); 1430 SaveAndNotifySettings(); 1431 return; 1432 } 1433 1434 if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal)) 1435 { 1436 SaveAndNotifySettings(); 1437 } 1438 } 1439 1440 private void InitializePasteAIProviderState() 1441 { 1442 var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration; 1443 if (pasteConfig is null) 1444 { 1445 _advancedPasteSettings.Properties.PasteAIConfiguration = new PasteAIConfiguration(); 1446 pasteConfig = _advancedPasteSettings.Properties.PasteAIConfiguration; 1447 } 1448 1449 pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>(); 1450 1451 SubscribeToPasteAIProviders(pasteConfig); 1452 } 1453 1454 private static string RetrieveCredentialValue(string credentialResource, string credentialUserName) 1455 { 1456 if (string.IsNullOrWhiteSpace(credentialResource) || string.IsNullOrWhiteSpace(credentialUserName)) 1457 { 1458 return string.Empty; 1459 } 1460 1461 try 1462 { 1463 PasswordVault vault = new(); 1464 PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName); 1465 existingCred?.RetrievePassword(); 1466 return existingCred?.Password?.Trim() ?? string.Empty; 1467 } 1468 catch (Exception) 1469 { 1470 return string.Empty; 1471 } 1472 } 1473 1474 private void UpdateCustomActionsCanMoveUpDown() 1475 { 1476 for (int index = 0; index < _customActions.Count; index++) 1477 { 1478 var customAction = _customActions[index]; 1479 customAction.CanMoveUp = index != 0; 1480 customAction.CanMoveDown = index != _customActions.Count - 1; 1481 } 1482 } 1483 } 1484 }