/ src / settings-ui / Settings.UI / ViewModels / ColorPickerViewModel.cs
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  }