ProviderSettingsViewModel.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.Diagnostics.CodeAnalysis; 6 using CommunityToolkit.Mvvm.ComponentModel; 7 using CommunityToolkit.Mvvm.Messaging; 8 using Microsoft.CmdPal.Core.Common.Services; 9 using Microsoft.CmdPal.Core.ViewModels; 10 using Microsoft.CmdPal.UI.ViewModels.Messages; 11 using Microsoft.CmdPal.UI.ViewModels.Properties; 12 13 namespace Microsoft.CmdPal.UI.ViewModels; 14 15 public partial class ProviderSettingsViewModel : ObservableObject 16 { 17 private static readonly IconInfoViewModel EmptyIcon = new(null); 18 19 private readonly CommandProviderWrapper _provider; 20 private readonly ProviderSettings _providerSettings; 21 private readonly SettingsModel _settings; 22 private readonly Lock _initializeSettingsLock = new(); 23 24 private Task? _initializeSettingsTask; 25 26 public ProviderSettingsViewModel( 27 CommandProviderWrapper provider, 28 ProviderSettings providerSettings, 29 SettingsModel settings) 30 { 31 _provider = provider; 32 _providerSettings = providerSettings; 33 _settings = settings; 34 35 LoadingSettings = _provider.Settings?.HasSettings ?? false; 36 37 BuildFallbackViewModels(); 38 } 39 40 public string DisplayName => _provider.DisplayName; 41 42 public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? "Built-in"; 43 44 public string ExtensionSubtext => IsEnabled ? 45 HasFallbackCommands ? 46 $"{ExtensionName}, {TopLevelCommands.Count} commands, {_provider.FallbackItems?.Length} fallback commands" : 47 $"{ExtensionName}, {TopLevelCommands.Count} commands" : 48 $"{ExtensionName}, {Resources.builtin_disabled_extension}"; 49 50 [MemberNotNullWhen(true, nameof(Extension))] 51 public bool IsFromExtension => _provider.Extension is not null; 52 53 public IExtensionWrapper? Extension => _provider.Extension; 54 55 public string ExtensionVersion => IsFromExtension ? $"{Extension.Version.Major}.{Extension.Version.Minor}.{Extension.Version.Build}.{Extension.Version.Revision}" : string.Empty; 56 57 public IconInfoViewModel Icon => IsEnabled ? _provider.Icon : EmptyIcon; 58 59 [ObservableProperty] 60 public partial bool LoadingSettings { get; set; } 61 62 public bool IsEnabled 63 { 64 get => _providerSettings.IsEnabled; 65 set 66 { 67 if (value != _providerSettings.IsEnabled) 68 { 69 _providerSettings.IsEnabled = value; 70 Save(); 71 WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new()); 72 OnPropertyChanged(nameof(IsEnabled)); 73 OnPropertyChanged(nameof(ExtensionSubtext)); 74 OnPropertyChanged(nameof(Icon)); 75 } 76 77 if (value == true) 78 { 79 _provider.CommandsChanged -= Provider_CommandsChanged; 80 _provider.CommandsChanged += Provider_CommandsChanged; 81 } 82 } 83 } 84 85 /// <summary> 86 /// Gets a value indicating whether returns true if we have a settings page 87 /// that's initialized, or we are still working on initializing that 88 /// settings page. If we don't have a settings object, or that settings 89 /// object doesn't have a settings page, then we'll return false. 90 /// </summary> 91 public bool HasSettings 92 { 93 get 94 { 95 if (_provider.Settings is null) 96 { 97 return false; 98 } 99 100 if (_provider.Settings.Initialized) 101 { 102 return _provider.Settings.HasSettings; 103 } 104 105 // settings still need to be loaded. 106 return LoadingSettings; 107 } 108 } 109 110 /// <summary> 111 /// Gets will return the settings page, if we have one, and have initialized it. 112 /// If we haven't initialized it, this will kick off a thread to start 113 /// initializing it. 114 /// </summary> 115 public ContentPageViewModel? SettingsPage 116 { 117 get 118 { 119 if (_provider.Settings is null) 120 { 121 return null; 122 } 123 124 if (_provider.Settings.Initialized) 125 { 126 LoadingSettings = false; 127 return _provider.Settings.SettingsPage; 128 } 129 130 // Don't load the settings if we're already working on it 131 lock (_initializeSettingsLock) 132 { 133 _initializeSettingsTask ??= Task.Run(InitializeSettingsPage); 134 } 135 136 return null; 137 } 138 } 139 140 [field: AllowNull] 141 public List<TopLevelViewModel> TopLevelCommands 142 { 143 get 144 { 145 if (field is null) 146 { 147 field = BuildTopLevelViewModels(); 148 } 149 150 return field; 151 } 152 } 153 154 private List<TopLevelViewModel> BuildTopLevelViewModels() 155 { 156 var thisProvider = _provider; 157 var providersCommands = thisProvider.TopLevelItems; 158 159 // Remember! This comes in on the UI thread! 160 return [.. providersCommands]; 161 } 162 163 [field: AllowNull] 164 public List<FallbackSettingsViewModel> FallbackCommands { get; set; } = []; 165 166 public bool HasFallbackCommands => _provider.FallbackItems?.Length > 0; 167 168 private void BuildFallbackViewModels() 169 { 170 var thisProvider = _provider; 171 var providersFallbackCommands = thisProvider.FallbackItems; 172 173 List<FallbackSettingsViewModel> fallbackViewModels = new(providersFallbackCommands.Length); 174 foreach (var fallbackItem in providersFallbackCommands) 175 { 176 if (_providerSettings.FallbackCommands.TryGetValue(fallbackItem.Id, out var fallbackSettings)) 177 { 178 fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, _settings, this)); 179 } 180 else 181 { 182 fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), _settings, this)); 183 } 184 } 185 186 FallbackCommands = fallbackViewModels; 187 } 188 189 private void Save() => SettingsModel.SaveSettings(_settings); 190 191 private void InitializeSettingsPage() 192 { 193 if (_provider.Settings is null) 194 { 195 return; 196 } 197 198 _provider.Settings.SafeInitializeProperties(); 199 _provider.Settings.DoOnUiThread(() => 200 { 201 // Changing these properties will try to update XAML, and that has 202 // to be handled on the UI thread, so we need to raise them on the 203 // UI thread 204 LoadingSettings = false; 205 OnPropertyChanged(nameof(HasSettings)); 206 OnPropertyChanged(nameof(LoadingSettings)); 207 OnPropertyChanged(nameof(SettingsPage)); 208 }); 209 } 210 211 private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args) 212 { 213 OnPropertyChanged(nameof(ExtensionSubtext)); 214 OnPropertyChanged(nameof(TopLevelCommands)); 215 } 216 }