ColorPickerViewModel.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.Globalization; 9 using System.Linq; 10 using System.Text.Json; 11 using System.Timers; 12 using global::PowerToys.GPOWrapper; 13 using ManagedCommon; 14 using Microsoft.PowerToys.Settings.UI.Helpers; 15 using Microsoft.PowerToys.Settings.UI.Library; 16 using Microsoft.PowerToys.Settings.UI.Library.Enumerations; 17 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 18 using Microsoft.PowerToys.Settings.UI.Library.Interfaces; 19 using Microsoft.PowerToys.Settings.UI.SerializationContext; 20 21 namespace Microsoft.PowerToys.Settings.UI.ViewModels 22 { 23 public partial class ColorPickerViewModel : PageViewModelBase 24 { 25 protected override string ModuleName => ColorPickerSettings.ModuleName; 26 27 private bool _disposed; 28 29 // Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval 30 private const int SaveSettingsDelayInMs = 500; 31 32 private GeneralSettings GeneralSettingsConfig { get; set; } 33 34 private readonly SettingsUtils _settingsUtils; 35 private readonly System.Threading.Lock _delayedActionLock = new System.Threading.Lock(); 36 37 private readonly ColorPickerSettings _colorPickerSettings; 38 private Timer _delayedTimer; 39 40 private GpoRuleConfigured _enabledGpoRuleConfiguration; 41 private bool _enabledStateIsGPOConfigured; 42 private bool _isEnabled; 43 private int _colorFormatPreviewIndex; 44 45 private Func<string, int> SendConfigMSG { get; } 46 47 private Dictionary<string, string> _colorFormatsPreview; 48 49 public ColorPickerViewModel( 50 SettingsUtils settingsUtils, 51 ISettingsRepository<GeneralSettings> settingsRepository, 52 ISettingsRepository<ColorPickerSettings> colorPickerSettingsRepository, 53 Func<string, int> ipcMSGCallBackFunc) 54 { 55 // Obtain the general PowerToy settings configurations 56 ArgumentNullException.ThrowIfNull(settingsRepository); 57 58 GeneralSettingsConfig = settingsRepository.SettingsConfig; 59 60 _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); 61 62 _colorPickerSettings = colorPickerSettingsRepository.SettingsConfig; 63 64 InitializeEnabledValue(); 65 66 // set the callback functions value to handle outgoing IPC message. 67 SendConfigMSG = ipcMSGCallBackFunc; 68 69 _delayedTimer = new Timer(); 70 _delayedTimer.Interval = SaveSettingsDelayInMs; 71 _delayedTimer.Elapsed += DelayedTimer_Tick; 72 _delayedTimer.AutoReset = false; 73 74 InitializeColorFormats(); 75 } 76 77 private void InitializeEnabledValue() 78 { 79 _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredColorPickerEnabledValue(); 80 if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) 81 { 82 // Get the enabled state from GPO. 83 _enabledStateIsGPOConfigured = true; 84 _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; 85 } 86 else 87 { 88 _isEnabled = GeneralSettingsConfig.Enabled.ColorPicker; 89 } 90 } 91 92 public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings() 93 { 94 var hotkeysDict = new Dictionary<string, HotkeySettings[]> 95 { 96 [ModuleName] = [ActivationShortcut], 97 }; 98 99 return hotkeysDict; 100 } 101 102 public bool IsEnabled 103 { 104 get => _isEnabled; 105 set 106 { 107 if (_enabledStateIsGPOConfigured) 108 { 109 // If it's GPO configured, shouldn't be able to change this state. 110 return; 111 } 112 113 if (_isEnabled != value) 114 { 115 _isEnabled = value; 116 OnPropertyChanged(nameof(IsEnabled)); 117 118 // Set the status of ColorPicker in the general settings 119 GeneralSettingsConfig.Enabled.ColorPicker = value; 120 var outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); 121 122 SendConfigMSG(outgoing.ToString()); 123 } 124 } 125 } 126 127 public bool IsEnabledGpoConfigured 128 { 129 get => _enabledStateIsGPOConfigured; 130 } 131 132 public bool ChangeCursor 133 { 134 get => _colorPickerSettings.Properties.ChangeCursor; 135 set 136 { 137 if (_colorPickerSettings.Properties.ChangeCursor != value) 138 { 139 _colorPickerSettings.Properties.ChangeCursor = value; 140 OnPropertyChanged(nameof(ChangeCursor)); 141 NotifySettingsChanged(); 142 } 143 } 144 } 145 146 public HotkeySettings ActivationShortcut 147 { 148 get => _colorPickerSettings.Properties.ActivationShortcut; 149 set 150 { 151 if (_colorPickerSettings.Properties.ActivationShortcut != value) 152 { 153 _colorPickerSettings.Properties.ActivationShortcut = value ?? _colorPickerSettings.Properties.DefaultActivationShortcut; 154 OnPropertyChanged(nameof(ActivationShortcut)); 155 NotifySettingsChanged(); 156 } 157 } 158 } 159 160 public string SelectedColorRepresentationValue 161 { 162 get => _colorPickerSettings.Properties.CopiedColorRepresentation; 163 set 164 { 165 if (value == null) 166 { 167 return; // do not set null value, it occurs when the combobox itemSource gets modified. Right after it well be reset to the correct value 168 } 169 170 if (_colorPickerSettings.Properties.CopiedColorRepresentation != value) 171 { 172 _colorPickerSettings.Properties.CopiedColorRepresentation = value; 173 OnPropertyChanged(nameof(SelectedColorRepresentationValue)); 174 NotifySettingsChanged(); 175 } 176 } 177 } 178 179 public int ActivationBehavior 180 { 181 get 182 { 183 return (int)_colorPickerSettings.Properties.ActivationAction; 184 } 185 186 set 187 { 188 if (value != (int)_colorPickerSettings.Properties.ActivationAction) 189 { 190 _colorPickerSettings.Properties.ActivationAction = (ColorPickerActivationAction)value; 191 OnPropertyChanged(nameof(ActivationBehavior)); 192 NotifySettingsChanged(); 193 } 194 } 195 } 196 197 public int PrimaryClickBehavior 198 { 199 get => (int)_colorPickerSettings.Properties.PrimaryClickAction; 200 201 set 202 { 203 if (value != (int)_colorPickerSettings.Properties.PrimaryClickAction) 204 { 205 _colorPickerSettings.Properties.PrimaryClickAction = (ColorPickerClickAction)value; 206 OnPropertyChanged(nameof(PrimaryClickBehavior)); 207 NotifySettingsChanged(); 208 } 209 } 210 } 211 212 public int MiddleClickBehavior 213 { 214 get => (int)_colorPickerSettings.Properties.MiddleClickAction; 215 216 set 217 { 218 if (value != (int)_colorPickerSettings.Properties.MiddleClickAction) 219 { 220 _colorPickerSettings.Properties.MiddleClickAction = (ColorPickerClickAction)value; 221 OnPropertyChanged(nameof(MiddleClickBehavior)); 222 NotifySettingsChanged(); 223 } 224 } 225 } 226 227 public int SecondaryClickBehavior 228 { 229 get => (int)_colorPickerSettings.Properties.SecondaryClickAction; 230 231 set 232 { 233 if (value != (int)_colorPickerSettings.Properties.SecondaryClickAction) 234 { 235 _colorPickerSettings.Properties.SecondaryClickAction = (ColorPickerClickAction)value; 236 OnPropertyChanged(nameof(SecondaryClickBehavior)); 237 NotifySettingsChanged(); 238 } 239 } 240 } 241 242 public bool ShowColorName 243 { 244 get => _colorPickerSettings.Properties.ShowColorName; 245 set 246 { 247 if (_colorPickerSettings.Properties.ShowColorName != value) 248 { 249 _colorPickerSettings.Properties.ShowColorName = value; 250 OnPropertyChanged(nameof(ShowColorName)); 251 NotifySettingsChanged(); 252 } 253 } 254 } 255 256 public ObservableCollection<ColorFormatModel> ColorFormats { get; } = new ObservableCollection<ColorFormatModel>(); 257 258 public Dictionary<string, string> ColorFormatsPreview 259 { 260 get => _colorFormatsPreview; 261 set 262 { 263 _colorFormatsPreview = value; 264 OnPropertyChanged(nameof(ColorFormatsPreview)); 265 } 266 } 267 268 public int ColorFormatsPreviewIndex 269 { 270 get 271 { 272 return _colorFormatPreviewIndex; 273 } 274 275 set 276 { 277 if (value != _colorFormatPreviewIndex) 278 { 279 _colorFormatPreviewIndex = value; 280 OnPropertyChanged(nameof(ColorFormatsPreviewIndex)); 281 } 282 } 283 } 284 285 private void InitializeColorFormats() 286 { 287 foreach (var storedColorFormat in _colorPickerSettings.Properties.VisibleColorFormats) 288 { 289 // skip entries with empty name or duplicated name, it should never occur 290 string storedName = storedColorFormat.Key; 291 if (storedName == string.Empty || ColorFormats.Any(x => x.Name.ToUpperInvariant().Equals(storedName.ToUpperInvariant(), StringComparison.Ordinal))) 292 { 293 continue; 294 } 295 296 string format = storedColorFormat.Value.Value; 297 if (format == string.Empty) 298 { 299 format = ColorFormatHelper.GetDefaultFormat(storedName); 300 } 301 302 ColorFormatModel customColorFormat = new ColorFormatModel(storedName, format, storedColorFormat.Value.Key); 303 customColorFormat.PropertyChanged += ColorFormat_PropertyChanged; 304 ColorFormats.Add(customColorFormat); 305 } 306 307 // Reordering colors with buttons: disable first and last buttons 308 ColorFormats[0].CanMoveUp = false; 309 ColorFormats[ColorFormats.Count - 1].CanMoveDown = false; 310 311 UpdateColorFormatPreview(); 312 ColorFormats.CollectionChanged += ColorFormats_CollectionChanged; 313 } 314 315 private void UpdateColorFormatPreview() 316 { 317 ColorFormatsPreview = ColorFormats.Select(x => new KeyValuePair<string, string>(x.Name, x.Name + " - " + x.Example)).ToDictionary(x => x.Key, x => x.Value); 318 SetPreviewSelectedIndex(); 319 ScheduleSavingOfSettings(); 320 } 321 322 private void ColorFormats_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 323 { 324 // Reordering colors with buttons: update buttons availability depending on order 325 if (ColorFormats.Count > 0) 326 { 327 foreach (var color in ColorFormats) 328 { 329 color.CanMoveUp = true; 330 color.CanMoveDown = true; 331 } 332 333 ColorFormats[0].CanMoveUp = false; 334 ColorFormats[ColorFormats.Count - 1].CanMoveDown = false; 335 } 336 337 if (ColorFormats.Count == 1) 338 { 339 ColorFormats.Single().CanBeDeleted = false; 340 } 341 else 342 { 343 foreach (var color in ColorFormats) 344 { 345 color.CanBeDeleted = true; 346 } 347 } 348 349 UpdateColorFormats(); 350 UpdateColorFormatPreview(); 351 } 352 353 private void ColorFormat_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) 354 { 355 // Remaining properties are handled by the collection and by the dialog 356 if (e.PropertyName == nameof(ColorFormatModel.IsShown)) 357 { 358 UpdateColorFormats(); 359 ScheduleSavingOfSettings(); 360 } 361 } 362 363 private void ScheduleSavingOfSettings() 364 { 365 lock (_delayedActionLock) 366 { 367 if (_delayedTimer.Enabled) 368 { 369 _delayedTimer.Stop(); 370 } 371 372 _delayedTimer.Start(); 373 } 374 } 375 376 private void DelayedTimer_Tick(object sender, EventArgs e) 377 { 378 lock (_delayedActionLock) 379 { 380 _delayedTimer.Stop(); 381 NotifySettingsChanged(); 382 } 383 } 384 385 private void UpdateColorFormats() 386 { 387 _colorPickerSettings.Properties.VisibleColorFormats.Clear(); 388 foreach (var colorFormat in ColorFormats) 389 { 390 _colorPickerSettings.Properties.VisibleColorFormats.Add(colorFormat.Name, new KeyValuePair<bool, string>(colorFormat.IsShown, colorFormat.Format)); 391 } 392 } 393 394 internal void AddNewColorFormat(string newColorName, string newColorFormat, bool isShown) 395 { 396 if (ColorFormats.Count > 0) 397 { 398 ColorFormats[0].CanMoveUp = true; 399 } 400 401 ColorFormatModel newModel = new ColorFormatModel(newColorName, newColorFormat, isShown); 402 newModel.PropertyChanged += ColorFormat_PropertyChanged; 403 ColorFormats.Insert(0, newModel); 404 SetPreviewSelectedIndex(); 405 } 406 407 private void NotifySettingsChanged() 408 { 409 // Using InvariantCulture as this is an IPC message 410 SendConfigMSG( 411 string.Format( 412 CultureInfo.InvariantCulture, 413 "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", 414 ColorPickerSettings.ModuleName, 415 JsonSerializer.Serialize(_colorPickerSettings, SourceGenerationContextContext.Default.ColorPickerSettings))); 416 } 417 418 public void RefreshEnabledState() 419 { 420 InitializeEnabledValue(); 421 OnPropertyChanged(nameof(IsEnabled)); 422 } 423 424 protected override void Dispose(bool disposing) 425 { 426 if (!_disposed) 427 { 428 if (disposing) 429 { 430 _delayedTimer?.Dispose(); 431 foreach (var colorFormat in ColorFormats) 432 { 433 colorFormat.PropertyChanged -= ColorFormat_PropertyChanged; 434 } 435 436 ColorFormats.CollectionChanged -= ColorFormats_CollectionChanged; 437 } 438 439 _disposed = true; 440 } 441 442 base.Dispose(disposing); 443 } 444 445 internal ColorFormatModel GetNewColorFormatModel() 446 { 447 var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; 448 string defaultName = resourceLoader.GetString("CustomColorFormatDefaultName"); 449 ColorFormatModel newColorFormatModel = new ColorFormatModel(); 450 newColorFormatModel.Name = defaultName; 451 int extensionNumber = 1; 452 while (ColorFormats.Any(x => x.Name.Equals(newColorFormatModel.Name, StringComparison.Ordinal))) 453 { 454 newColorFormatModel.Name = defaultName + " (" + extensionNumber + ")"; 455 extensionNumber++; 456 } 457 458 return newColorFormatModel; 459 } 460 461 internal bool SetValidity(ColorFormatModel colorFormatModel, string oldName) 462 { 463 if ((colorFormatModel.Format == string.Empty) || (colorFormatModel.Name == string.Empty)) 464 { 465 colorFormatModel.IsValid = false; 466 } 467 else if (colorFormatModel.Name == oldName) 468 { 469 colorFormatModel.IsValid = true; 470 } 471 else 472 { 473 colorFormatModel.IsValid = ColorFormats.Count(x => x.Name.ToUpperInvariant().Equals(colorFormatModel.Name.ToUpperInvariant(), StringComparison.Ordinal)) 474 < (colorFormatModel.IsNew ? 1 : 2); 475 } 476 477 return colorFormatModel.IsValid; 478 } 479 480 internal int DeleteModel(ColorFormatModel colorFormatModel) 481 { 482 var deleteIndex = ColorFormats.IndexOf(colorFormatModel); 483 ColorFormats.Remove(colorFormatModel); 484 return deleteIndex; 485 } 486 487 internal void UpdateColorFormat(string oldName, ColorFormatModel colorFormat) 488 { 489 if (SelectedColorRepresentationValue == oldName) 490 { 491 SelectedColorRepresentationValue = colorFormat.Name; // name might be changed by the user 492 } 493 494 UpdateColorFormats(); 495 UpdateColorFormatPreview(); 496 } 497 498 internal void SetPreviewSelectedIndex() 499 { 500 int index = 0; 501 502 foreach (var item in ColorFormats) 503 { 504 if (item.Name == SelectedColorRepresentationValue) 505 { 506 break; 507 } 508 509 index++; 510 } 511 512 if (index >= ColorFormats.Count) 513 { 514 index = 0; 515 } 516 517 ColorFormatsPreviewIndex = index; 518 } 519 } 520 }