From 003c4fab448275095abb0f22a6c9aec523510051 Mon Sep 17 00:00:00 2001 From: Michael Clayton Date: Thu, 28 Sep 2023 00:05:30 +0100 Subject: [PATCH] [MouseJump] - add FileSystemWatcher for config (#26703) --- .../MouseJumpUI/Helpers/SettingsHelper.cs | 95 +++++++++++++++++++ .../Helpers/ThrottledActionInvoker.cs | 47 +++++++++ .../MouseUtils/MouseJumpUI/MainForm.cs | 13 ++- src/modules/MouseUtils/MouseJumpUI/Program.cs | 5 +- .../Settings.UI.Library/MouseJumpSettings.cs | 18 ++++ 5 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs new file mode 100644 index 00000000000..c2602ad7002 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.IO.Abstractions; +using System.Threading; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; + +namespace MouseJumpUI.Helpers; + +internal class SettingsHelper +{ + public SettingsHelper() + { + this.LockObject = new(); + this.CurrentSettings = this.LoadSettings(); + + // delay loading settings on change by some time to avoid file in use exception + var throttledActionInvoker = new ThrottledActionInvoker(); + this.FileSystemWatcher = Helper.GetFileWatcher( + moduleName: MouseJumpSettings.ModuleName, + fileName: "settings.json", + onChangedCallback: () => throttledActionInvoker.ScheduleAction(this.ReloadSettings, 250)); + } + + private IFileSystemWatcher FileSystemWatcher + { + get; + } + + private object LockObject + { + get; + } + + public MouseJumpSettings CurrentSettings + { + get; + private set; + } + + private MouseJumpSettings LoadSettings() + { + lock (this.LockObject) + { + { + var settingsUtils = new SettingsUtils(); + + // set this to 1 to disable retries + var remainingRetries = 5; + + while (remainingRetries > 0) + { + try + { + if (!settingsUtils.SettingsExists(MouseJumpSettings.ModuleName)) + { + Logger.LogInfo("MouseJump settings.json was missing, creating a new one"); + var defaultSettings = new MouseJumpSettings(); + defaultSettings.Save(settingsUtils); + } + + var settings = settingsUtils.GetSettingsOrDefault(MouseJumpSettings.ModuleName); + return settings; + } + catch (IOException ex) + { + Logger.LogError("Failed to read changed settings", ex); + Thread.Sleep(250); + } + catch (Exception ex) + { + Logger.LogError("Failed to read changed settings", ex); + Thread.Sleep(250); + } + + remainingRetries--; + } + } + } + + const string message = "Failed to read changed settings - ran out of retries"; + Logger.LogError(message); + throw new InvalidOperationException(message); + } + + public void ReloadSettings() + { + this.CurrentSettings = this.LoadSettings(); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs new file mode 100644 index 00000000000..ecd17cfa4c7 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Windows.Threading; + +namespace MouseJumpUI.Helpers; + +internal sealed class ThrottledActionInvoker +{ + private readonly object _invokerLock = new(); + private readonly DispatcherTimer _timer; + + private Action? _actionToRun; + + public ThrottledActionInvoker() + { + _timer = new DispatcherTimer(); + _timer.Tick += Timer_Tick; + } + + public void ScheduleAction(Action action, int milliseconds) + { + lock (_invokerLock) + { + if (_timer.IsEnabled) + { + _timer.Stop(); + } + + _actionToRun = action; + _timer.Interval = new TimeSpan(0, 0, 0, 0, milliseconds); + + _timer.Start(); + } + } + + private void Timer_Tick(object? sender, EventArgs? e) + { + lock (_invokerLock) + { + _timer.Stop(); + _actionToRun?.Invoke(); + } + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs index 8a46bb51a14..d7079214b78 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs @@ -19,13 +19,13 @@ namespace MouseJumpUI; internal partial class MainForm : Form { - public MainForm(MouseJumpSettings settings) + public MainForm(SettingsHelper settingsHelper) { this.InitializeComponent(); - this.Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper)); } - public MouseJumpSettings Settings + public SettingsHelper SettingsHelper { get; } @@ -171,6 +171,9 @@ private static LayoutInfo GetLayoutInfo(MainForm form) .Single(item => item.Screen.Handle == activatedScreenHandle.Value) .Index; + // avoid a race condition - cache the current settings in case they change + var currentSettings = form.SettingsHelper.CurrentSettings; + var layoutConfig = new LayoutConfig( virtualScreenBounds: ScreenHelper.GetVirtualScreen(), screens: screens.Select(item => item.Screen).ToList(), @@ -178,8 +181,8 @@ private static LayoutInfo GetLayoutInfo(MainForm form) activatedScreenIndex: activatedScreenIndex, activatedScreenNumber: activatedScreenIndex + 1, maximumFormSize: new( - form.Settings.Properties.ThumbnailSize.Width, - form.Settings.Properties.ThumbnailSize.Height), + currentSettings.Properties.ThumbnailSize.Width, + currentSettings.Properties.ThumbnailSize.Height), /* don't read the panel padding values because they are affected by dpi scaling and can give wrong values when moving between monitors with different dpi scaling diff --git a/src/modules/MouseUtils/MouseJumpUI/Program.cs b/src/modules/MouseUtils/MouseJumpUI/Program.cs index f0c05c74912..d70ebfa4543 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Program.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Program.cs @@ -13,6 +13,7 @@ using interop; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; +using MouseJumpUI.Helpers; namespace MouseJumpUI; @@ -69,8 +70,8 @@ private static void Main(string[] args) Application.Exit(); }); - var settings = Program.ReadSettings(); - var mainForm = new MainForm(settings); + var settingsHelper = new SettingsHelper(); + var mainForm = new MainForm(settingsHelper); NativeEventWaiter.WaitForEventLoop( Constants.MouseJumpShowPreviewEvent(), diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs index 73b15f50863..2c0cf9eb3bb 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs @@ -2,6 +2,8 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -21,6 +23,22 @@ public MouseJumpSettings() Version = "1.0"; } + public void Save(ISettingsUtils settingsUtils) + { + // Save settings to file + var options = new JsonSerializerOptions + { + WriteIndented = true, + }; + + if (settingsUtils == null) + { + throw new ArgumentNullException(nameof(settingsUtils)); + } + + settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName); + } + public string GetModuleName() { return Name;