/ src / modules / cmdpal / Microsoft.CmdPal.UI.ViewModels / ProviderSettingsViewModel.cs
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  }