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 }