/ src / settings-ui / Settings.UI / ViewModels / PeekViewModel.cs
PeekViewModel.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.Globalization;
  8  using System.IO;
  9  using System.IO.Abstractions;
 10  using System.Text.Json;
 11  using global::PowerToys.GPOWrapper;
 12  using ManagedCommon;
 13  using Microsoft.PowerToys.Settings.UI.Helpers;
 14  using Microsoft.PowerToys.Settings.UI.Library;
 15  using Microsoft.PowerToys.Settings.UI.Library.Helpers;
 16  using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
 17  using Microsoft.PowerToys.Settings.UI.Library.Utilities;
 18  using Microsoft.PowerToys.Settings.UI.SerializationContext;
 19  using Microsoft.UI.Dispatching;
 20  using Settings.UI.Library;
 21  
 22  namespace Microsoft.PowerToys.Settings.UI.ViewModels
 23  {
 24      public class PeekViewModel : PageViewModelBase
 25      {
 26          protected override string ModuleName => PeekSettings.ModuleName;
 27  
 28          private bool _isEnabled;
 29  
 30          private bool _disposed;
 31  
 32          private bool _settingsUpdating;
 33  
 34          private GeneralSettings GeneralSettingsConfig { get; set; }
 35  
 36          private readonly DispatcherQueue _dispatcherQueue;
 37  
 38          private readonly SettingsUtils _settingsUtils;
 39          private readonly PeekPreviewSettings _peekPreviewSettings;
 40          private PeekSettings _peekSettings;
 41  
 42          private GpoRuleConfigured _enabledGpoRuleConfiguration;
 43          private bool _enabledStateIsGPOConfigured;
 44  
 45          private Func<string, int> SendConfigMSG { get; }
 46  
 47          private IFileSystemWatcher _watcher;
 48  
 49          public PeekViewModel(
 50              SettingsUtils settingsUtils,
 51              ISettingsRepository<GeneralSettings> settingsRepository,
 52              Func<string, int> ipcMSGCallBackFunc,
 53              DispatcherQueue dispatcherQueue)
 54          {
 55              // To obtain the general settings configurations of PowerToys Settings.
 56              ArgumentNullException.ThrowIfNull(settingsRepository);
 57  
 58              GeneralSettingsConfig = settingsRepository.SettingsConfig;
 59  
 60              _dispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue));
 61  
 62              _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
 63  
 64              // Load the application-specific settings, including preview items.
 65              _peekSettings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName);
 66              _peekPreviewSettings = _settingsUtils.GetSettingsOrDefault<PeekPreviewSettings>(PeekSettings.ModuleName, PeekPreviewSettings.FileName);
 67  
 68              SetupSettingsFileWatcher();
 69  
 70              InitializeEnabledValue();
 71  
 72              SendConfigMSG = ipcMSGCallBackFunc;
 73          }
 74  
 75          /// <summary>
 76          /// Set up the file watcher for the settings file. Used to respond to updates to the
 77          /// ConfirmFileDelete setting by the user within the Peek application itself.
 78          /// </summary>
 79          private void SetupSettingsFileWatcher()
 80          {
 81              string settingsPath = _settingsUtils.GetSettingsFilePath(PeekSettings.ModuleName);
 82  
 83              _watcher = Helper.GetFileWatcher(PeekSettings.ModuleName, SettingsUtils.DefaultFileName, () =>
 84              {
 85                  try
 86                  {
 87                      _settingsUpdating = true;
 88                      var newSettings = _settingsUtils.GetSettings<PeekSettings>(PeekSettings.ModuleName);
 89  
 90                      _dispatcherQueue.TryEnqueue(() =>
 91                      {
 92                          try
 93                          {
 94                              ConfirmFileDelete = newSettings.Properties.ConfirmFileDelete.Value;
 95                              _peekSettings = newSettings;
 96                          }
 97                          finally
 98                          {
 99                              // Only clear the flag once the UI update is complete.
100                              _settingsUpdating = false;
101                          }
102                      });
103                  }
104                  catch (Exception ex)
105                  {
106                      Logger.LogError($"Failed to load Peek settings: {ex.Message}", ex);
107                      _settingsUpdating = false;
108                  }
109              });
110          }
111  
112          private void InitializeEnabledValue()
113          {
114              _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredPeekEnabledValue();
115              if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
116              {
117                  // Get the enabled state from GPO.
118                  _enabledStateIsGPOConfigured = true;
119                  _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
120              }
121              else
122              {
123                  _isEnabled = GeneralSettingsConfig.Enabled.Peek;
124              }
125          }
126  
127          public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
128          {
129              var hotkeysDict = new Dictionary<string, HotkeySettings[]>
130              {
131                  [ModuleName] = [ActivationShortcut],
132              };
133  
134              return hotkeysDict;
135          }
136  
137          public bool IsEnabled
138          {
139              get => _isEnabled;
140              set
141              {
142                  if (_enabledStateIsGPOConfigured)
143                  {
144                      // If it's GPO configured, shouldn't be able to change this state.
145                      return;
146                  }
147  
148                  if (_isEnabled != value)
149                  {
150                      _isEnabled = value;
151  
152                      GeneralSettingsConfig.Enabled.Peek = value;
153                      OnPropertyChanged(nameof(IsEnabled));
154  
155                      OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
156                      SendConfigMSG(outgoing.ToString());
157                  }
158              }
159          }
160  
161          public bool IsEnabledGpoConfigured
162          {
163              get => _enabledStateIsGPOConfigured;
164          }
165  
166          public HotkeySettings ActivationShortcut
167          {
168              get => _peekSettings.Properties.ActivationShortcut;
169              set
170              {
171                  if (_peekSettings.Properties.ActivationShortcut != value)
172                  {
173                      // If space mode toggle is on, ignore external attempts to change (UI will be disabled, but defensive).
174                      if (EnableSpaceToActivate)
175                      {
176                          return;
177                      }
178  
179                      _peekSettings.Properties.ActivationShortcut = value ?? _peekSettings.Properties.DefaultActivationShortcut;
180                      OnPropertyChanged(nameof(ActivationShortcut));
181                      NotifySettingsChanged();
182                  }
183              }
184          }
185  
186          public bool AlwaysRunNotElevated
187          {
188              get => _peekSettings.Properties.AlwaysRunNotElevated.Value;
189              set
190              {
191                  if (_peekSettings.Properties.AlwaysRunNotElevated.Value != value)
192                  {
193                      _peekSettings.Properties.AlwaysRunNotElevated.Value = value;
194                      OnPropertyChanged(nameof(AlwaysRunNotElevated));
195                      NotifySettingsChanged();
196                  }
197              }
198          }
199  
200          public bool CloseAfterLosingFocus
201          {
202              get => _peekSettings.Properties.CloseAfterLosingFocus.Value;
203              set
204              {
205                  if (_peekSettings.Properties.CloseAfterLosingFocus.Value != value)
206                  {
207                      _peekSettings.Properties.CloseAfterLosingFocus.Value = value;
208                      OnPropertyChanged(nameof(CloseAfterLosingFocus));
209                      NotifySettingsChanged();
210                  }
211              }
212          }
213  
214          public bool ConfirmFileDelete
215          {
216              get => _peekSettings.Properties.ConfirmFileDelete.Value;
217              set
218              {
219                  if (_peekSettings.Properties.ConfirmFileDelete.Value != value)
220                  {
221                      _peekSettings.Properties.ConfirmFileDelete.Value = value;
222                      OnPropertyChanged(nameof(ConfirmFileDelete));
223                      NotifySettingsChanged();
224                  }
225              }
226          }
227  
228          public bool EnableSpaceToActivate
229          {
230              get => _peekSettings.Properties.EnableSpaceToActivate.Value;
231              set
232              {
233                  if (_peekSettings.Properties.EnableSpaceToActivate.Value != value)
234                  {
235                      _peekSettings.Properties.EnableSpaceToActivate.Value = value;
236  
237                      if (value)
238                      {
239                          // Force single space (0x20) without modifiers.
240                          _peekSettings.Properties.ActivationShortcut = new HotkeySettings(false, false, false, false, 0x20);
241                      }
242                      else
243                      {
244                          // Revert to default (design simplification, not restoring previous custom combo).
245                          _peekSettings.Properties.ActivationShortcut = _peekSettings.Properties.DefaultActivationShortcut;
246                      }
247  
248                      OnPropertyChanged(nameof(EnableSpaceToActivate));
249                      OnPropertyChanged(nameof(ActivationShortcut));
250                      NotifySettingsChanged();
251                  }
252              }
253          }
254  
255          public bool SourceCodeWrapText
256          {
257              get => _peekPreviewSettings.SourceCodeWrapText.Value;
258              set
259              {
260                  if (_peekPreviewSettings.SourceCodeWrapText.Value != value)
261                  {
262                      _peekPreviewSettings.SourceCodeWrapText.Value = value;
263                      OnPropertyChanged(nameof(SourceCodeWrapText));
264                      SavePreviewSettings();
265                  }
266              }
267          }
268  
269          public bool SourceCodeTryFormat
270          {
271              get => _peekPreviewSettings.SourceCodeTryFormat.Value;
272              set
273              {
274                  if (_peekPreviewSettings.SourceCodeTryFormat.Value != value)
275                  {
276                      _peekPreviewSettings.SourceCodeTryFormat.Value = value;
277                      OnPropertyChanged(nameof(SourceCodeTryFormat));
278                      SavePreviewSettings();
279                  }
280              }
281          }
282  
283          public int SourceCodeFontSize
284          {
285              get => _peekPreviewSettings.SourceCodeFontSize.Value;
286              set
287              {
288                  if (_peekPreviewSettings.SourceCodeFontSize.Value != value)
289                  {
290                      _peekPreviewSettings.SourceCodeFontSize.Value = value;
291                      OnPropertyChanged(nameof(SourceCodeFontSize));
292                      SavePreviewSettings();
293                  }
294              }
295          }
296  
297          public bool SourceCodeStickyScroll
298          {
299              get => _peekPreviewSettings.SourceCodeStickyScroll.Value;
300              set
301              {
302                  if (_peekPreviewSettings.SourceCodeStickyScroll.Value != value)
303                  {
304                      _peekPreviewSettings.SourceCodeStickyScroll.Value = value;
305                      OnPropertyChanged(nameof(SourceCodeStickyScroll));
306                      SavePreviewSettings();
307                  }
308              }
309          }
310  
311          public bool SourceCodeMinimap
312          {
313              get => _peekPreviewSettings.SourceCodeMinimap.Value;
314              set
315              {
316                  if (_peekPreviewSettings.SourceCodeMinimap.Value != value)
317                  {
318                      _peekPreviewSettings.SourceCodeMinimap.Value = value;
319                      OnPropertyChanged(nameof(SourceCodeMinimap));
320                      SavePreviewSettings();
321                  }
322              }
323          }
324  
325          private void NotifySettingsChanged()
326          {
327              // Do not send IPC message if the settings file has been updated by Peek itself.
328              if (_settingsUpdating)
329              {
330                  return;
331              }
332  
333              // This message will be intercepted by the runner, which passes the serialized JSON to
334              // Peek.set_config() in the C++ Peek project, which then saves it to file.
335              SendConfigMSG(
336                  string.Format(
337                      CultureInfo.InvariantCulture,
338                      "{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
339                      PeekSettings.ModuleName,
340                      JsonSerializer.Serialize(_peekSettings, SourceGenerationContextContext.Default.PeekSettings)));
341          }
342  
343          private void SavePreviewSettings()
344          {
345              _settingsUtils.SaveSettings(_peekPreviewSettings.ToJsonString(), PeekSettings.ModuleName, PeekPreviewSettings.FileName);
346          }
347  
348          public void RefreshEnabledState()
349          {
350              InitializeEnabledValue();
351              OnPropertyChanged(nameof(IsEnabled));
352          }
353  
354          protected override void Dispose(bool disposing)
355          {
356              if (!_disposed)
357              {
358                  if (disposing)
359                  {
360                      _watcher?.Dispose();
361                      _watcher = null;
362                  }
363  
364                  _disposed = true;
365              }
366  
367              base.Dispose(disposing);
368          }
369      }
370  }