From 82a63a45174bf3f8b0ca44c1b7253ddccd8e53e3 Mon Sep 17 00:00:00 2001 From: Michael Jolley Date: Tue, 9 Dec 2025 19:39:32 -0600 Subject: [PATCH] Continuing part deux --- PowerToys.slnx | 4 + .../AppStateModel.cs | 55 +------- .../SettingsModel.cs | 115 +---------------- .../AppStateService.cs | 50 ++++++++ .../CmdPalLogger.cs | 2 +- .../Extensions/BuiltInExtensionService.cs | 9 ++ .../Extensions/IExtensionService.cs | 9 ++ .../Extensions/WinRTExtensionService.cs | 9 ++ .../Helpers/ResourceLoaderInstance.cs | 2 +- .../JsonSerializationContext.cs | 4 +- ...icrosoft.CommandPalette.UI.Services.csproj | 1 + .../PersistenceService.cs | 2 +- .../SettingsService.cs | 121 ++++++++++++++++++ .../TrayIconService.cs | 34 ++++- ...rosoft.CommandPalette.UI.ViewModels.csproj | 1 + .../SettingsViewModel.cs | 84 +++++++----- .../Microsoft.CommandPalette.UI/App.xaml.cs | 21 +-- .../Converters/PlaceholderTextConverter.cs | 2 +- .../Helpers/GlobalErrorHandler.cs | 1 + .../MainWindow.xaml.cs | 38 +++--- .../Microsoft.CommandPalette.UI.csproj | 2 +- .../Pages/ShellPage.xaml.cs | 2 +- 22 files changed, 328 insertions(+), 240 deletions(-) create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/AppStateService.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/BuiltInExtensionService.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/IExtensionService.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/WinRTExtensionService.cs rename src/modules/Deux/UI/{Microsoft.CommandPalette.UI.Models => Microsoft.CommandPalette.UI.Services}/Helpers/ResourceLoaderInstance.cs (90%) rename src/modules/Deux/UI/{Microsoft.CommandPalette.UI.Models => Microsoft.CommandPalette.UI.Services}/JsonSerializationContext.cs (92%) rename src/modules/Deux/UI/{Microsoft.CommandPalette.UI.Models/Services => Microsoft.CommandPalette.UI.Services}/PersistenceService.cs (99%) create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/SettingsService.cs diff --git a/PowerToys.slnx b/PowerToys.slnx index 30dd1df814..57ac4fff86 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -369,6 +369,10 @@ + + + + diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.cs index b0ece6267b..4a89d4ce69 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.cs @@ -2,57 +2,14 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.CommandPalette.UI.Models; -using Microsoft.Extensions.Logging; -using Windows.Foundation; +namespace Microsoft.CommandPalette.UI.Models; -namespace Microsoft.CmdPal.UI.ViewModels; - -public partial class AppStateModel : ObservableObject +public partial class AppStateModel { - private static string _filePath; + /************************************************************************* + * Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)! + * Make sure that any new types you add are added to JsonSerializationContext! + *************************************************************************/ - public event TypedEventHandler? StateChanged; - - /////////////////////////////////////////////////////////////////////////// - // STATE HERE - // Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)! - // Make sure that any new types you add are added to JsonSerializationContext! public List RunHistory { get; set; } = []; - - // END STATE - /////////////////////////////////////////////////////////////////////////// - - static AppStateModel() - { - _filePath = PersistenceService.SettingsJsonPath("state.json"); - } - - public static AppStateModel LoadState(ILogger logger) - { - return PersistenceService.LoadObject(_filePath, JsonSerializationContext.Default.AppStateModel!, logger); - } - - public static void SaveState(AppStateModel model, ILogger logger) - { - try - { - PersistenceService.SaveObject( - model, - _filePath, - JsonSerializationContext.Default.AppStateModel!, - JsonSerializationContext.Default.AppStateModel!.Options, - beforeWriteMutation: null, - afterWriteCallback: m => m.StateChanged?.Invoke(m, null), - logger); - } - catch (Exception ex) - { - Log_SaveStateFailure(logger, _filePath, ex); - } - } - - [LoggerMessage(Level = LogLevel.Error, Message = "Failed to save application state to '{filePath}'.")] - static partial void Log_SaveStateFailure(ILogger logger, string filePath, Exception exception); } diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs index 1ccbe05f37..70ec6c21d2 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs @@ -2,27 +2,15 @@ // 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.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.Extensions.Logging; -using Windows.Foundation; - namespace Microsoft.CommandPalette.UI.Models; -public partial class SettingsModel : ObservableObject +public partial class SettingsModel { - private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome"; + /************************************************************************* + * Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)! + * Make sure that any new types you add are added to JsonSerializationContext! + *************************************************************************/ - [JsonIgnore] - private static readonly string _filePath; - - public event TypedEventHandler? SettingsChanged; - - /////////////////////////////////////////////////////////////////////////// - // SETTINGS HERE public static HotkeySettings DefaultActivationShortcut { get; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space public HotkeySettings? Hotkey { get; set; } = DefaultActivationShortcut; @@ -52,97 +40,4 @@ public partial class SettingsModel : ObservableObject public WindowPosition? LastWindowPosition { get; set; } public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan; - - // END SETTINGS - /////////////////////////////////////////////////////////////////////////// - - static SettingsModel() - { - _filePath = PersistenceService.SettingsJsonPath("settings.json"); - } - - private static bool ApplyMigrations(JsonObject root, SettingsModel model, ILogger logger) - { - var migrated = false; - - migrated |= TryMigrate( - "Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)", - root, - model, - nameof(AutoGoHomeInterval), - DeprecatedHotkeyGoesHomeKey, - (settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan, - JsonSerializationContext.Default.Boolean, - logger); - - return migrated; - } - - private static bool TryMigrate(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action apply, JsonTypeInfo jsonTypeInfo, ILogger logger) - { - try - { - if (root.ContainsKey(newKey) && root[newKey] is not null) - { - return false; - } - - if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null) - { - var value = oldNode.Deserialize(jsonTypeInfo); - apply(model, value!); - return true; - } - } - catch (Exception ex) - { - Log_MigrationFailure(logger, migrationName, ex); - } - - return false; - } - - public static SettingsModel LoadSettings(ILogger logger) - { - var settings = PersistenceService.LoadObject(_filePath, JsonSerializationContext.Default.SettingsModel!, logger); - - var migratedAny = false; - try - { - var jsonContent = File.Exists(_filePath) ? File.ReadAllText(_filePath) : "{}"; - if (JsonNode.Parse(jsonContent) is JsonObject root) - { - migratedAny |= ApplyMigrations(root, settings, logger); - } - } - catch (Exception ex) - { - Log_MigrationCheckFailure(logger, ex); - } - - if (migratedAny) - { - SaveSettings(settings, logger); - } - - return settings; - } - - public static void SaveSettings(SettingsModel model, ILogger logger) - { - PersistenceService.SaveObject( - model, - _filePath, - JsonSerializationContext.Default.SettingsModel, - JsonSerializationContext.Default.Options, - beforeWriteMutation: obj => obj.Remove(DeprecatedHotkeyGoesHomeKey), - afterWriteCallback: m => m.SettingsChanged?.Invoke(m, null), - logger); - } - - [LoggerMessage(Level = LogLevel.Error, Message = "Settings migration '{MigrationName}' failed.")] - static partial void Log_MigrationFailure(ILogger logger, string MigrationName, Exception exception); - - [LoggerMessage(Level = LogLevel.Error, Message = "Settings migration check failed.")] - static partial void Log_MigrationCheckFailure(ILogger logger, Exception exception); } diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/AppStateService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/AppStateService.cs new file mode 100644 index 0000000000..f377aa8fb2 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/AppStateService.cs @@ -0,0 +1,50 @@ +// 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 Microsoft.CommandPalette.UI.Models; +using Microsoft.Extensions.Logging; +using Windows.Foundation; + +namespace Microsoft.CommandPalette.UI.Services; + +public partial class AppStateService +{ + private readonly ILogger logger; + private readonly string _filePath; + private AppStateModel _appStateModel; + + public event TypedEventHandler? StateChanged; + + public AppStateModel CurrentSettings => _appStateModel; + + public AppStateService(ILogger logger) + { + this.logger = logger; + _filePath = PersistenceService.SettingsJsonPath("state.json"); + _appStateModel = LoadState(); + } + + private AppStateModel LoadState() + { + return PersistenceService.LoadObject(_filePath, JsonSerializationContext.Default.AppStateModel!, logger); + } + + public void SaveSettings(AppStateModel model) + { + PersistenceService.SaveObject( + model, + _filePath, + JsonSerializationContext.Default.AppStateModel, + JsonSerializationContext.Default.Options, + null, + afterWriteCallback: m => FinalizeStateSave(m), + logger); + } + + private void FinalizeStateSave(AppStateModel model) + { + _appStateModel = model; + StateChanged?.Invoke(model, null); + } +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/CmdPalLogger.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/CmdPalLogger.cs index 36a204b2cb..775191c8b8 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/CmdPalLogger.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/CmdPalLogger.cs @@ -9,7 +9,7 @@ namespace Microsoft.CommandPalette.UI.Services; // Adapter implementing Microsoft.Extensions.Logging.ILogger, // delegating to ManagedCommon.Logger. -internal sealed partial class CmdPalLogger : ILogger +public sealed partial class CmdPalLogger : ILogger { private static readonly AsyncLocal> _scopeStack = new(); private readonly LogLevel _minLevel; diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/BuiltInExtensionService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/BuiltInExtensionService.cs new file mode 100644 index 0000000000..f7062721ff --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/BuiltInExtensionService.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.CommandPalette.UI.Services.Extensions; + +internal class BuiltInExtensionService : IExtensionService +{ +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/IExtensionService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/IExtensionService.cs new file mode 100644 index 0000000000..4ea9d89c55 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/IExtensionService.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.CommandPalette.UI.Services.Extensions; + +internal interface IExtensionService +{ +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/WinRTExtensionService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/WinRTExtensionService.cs new file mode 100644 index 0000000000..4b3efc35e2 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Extensions/WinRTExtensionService.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.CommandPalette.UI.Services.Extensions; + +internal class WinRTExtensionService : IExtensionService +{ +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Helpers/ResourceLoaderInstance.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Helpers/ResourceLoaderInstance.cs similarity index 90% rename from src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Helpers/ResourceLoaderInstance.cs rename to src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Helpers/ResourceLoaderInstance.cs index 11bed01b92..c9eaf59f9b 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Helpers/ResourceLoaderInstance.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Helpers/ResourceLoaderInstance.cs @@ -4,7 +4,7 @@ using Microsoft.Windows.ApplicationModel.Resources; -namespace Microsoft.CommandPalette.UI.Models.Helpers; +namespace Microsoft.CommandPalette.UI.Services.Helpers; public static class ResourceLoaderInstance { diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/JsonSerializationContext.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/JsonSerializationContext.cs similarity index 92% rename from src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/JsonSerializationContext.cs rename to src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/JsonSerializationContext.cs index 0d5a2aa646..d47303e1f6 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/JsonSerializationContext.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/JsonSerializationContext.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; -using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CommandPalette.UI.Models; -namespace Microsoft.CommandPalette.UI.Models; +namespace Microsoft.CommandPalette.UI.Services; [JsonSerializable(typeof(float))] [JsonSerializable(typeof(int))] diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Microsoft.CommandPalette.UI.Services.csproj b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Microsoft.CommandPalette.UI.Services.csproj index 4739ff7fc4..d357131640 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Microsoft.CommandPalette.UI.Services.csproj +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Microsoft.CommandPalette.UI.Services.csproj @@ -29,6 +29,7 @@ + \ No newline at end of file diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Services/PersistenceService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/PersistenceService.cs similarity index 99% rename from src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Services/PersistenceService.cs rename to src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/PersistenceService.cs index 083f51045d..138e32f166 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Services/PersistenceService.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/PersistenceService.cs @@ -9,7 +9,7 @@ using System.Text.Json.Serialization.Metadata; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.Logging; -namespace Microsoft.CommandPalette.UI.Models; +namespace Microsoft.CommandPalette.UI.Services; public partial class PersistenceService { diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/SettingsService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/SettingsService.cs new file mode 100644 index 0000000000..658b4a5c45 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/SettingsService.cs @@ -0,0 +1,121 @@ +// 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.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; +using Microsoft.CommandPalette.UI.Models; +using Microsoft.Extensions.Logging; +using Windows.Foundation; + +namespace Microsoft.CommandPalette.UI.Services; + +public partial class SettingsService +{ + private readonly ILogger logger; + private readonly string _filePath; + private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome"; + private SettingsModel _settingsModel; + + public event TypedEventHandler? SettingsChanged; + + public SettingsModel CurrentSettings => _settingsModel; + + public SettingsService(ILogger logger) + { + this.logger = logger; + _filePath = PersistenceService.SettingsJsonPath("settings.json"); + _settingsModel = LoadSettings(); + } + + private SettingsModel LoadSettings() + { + var settings = PersistenceService.LoadObject(_filePath, JsonSerializationContext.Default.SettingsModel!, logger); + + var migratedAny = false; + try + { + var jsonContent = File.Exists(_filePath) ? File.ReadAllText(_filePath) : "{}"; + if (JsonNode.Parse(jsonContent) is JsonObject root) + { + migratedAny |= ApplyMigrations(root, settings); + } + } + catch (Exception ex) + { + Log_MigrationCheckFailure(ex); + } + + if (migratedAny) + { + SaveSettings(settings); + } + + return settings; + } + + public void SaveSettings(SettingsModel model) + { + PersistenceService.SaveObject( + model, + _filePath, + JsonSerializationContext.Default.SettingsModel, + JsonSerializationContext.Default.Options, + beforeWriteMutation: obj => obj.Remove(DeprecatedHotkeyGoesHomeKey), + afterWriteCallback: m => FinalizeSettingsSave(m), + logger); + } + + private void FinalizeSettingsSave(SettingsModel model) + { + _settingsModel = model; + SettingsChanged?.Invoke(model, null); + } + + private bool ApplyMigrations(JsonObject root, SettingsModel model) + { + var migrated = false; + + migrated |= TryMigrate( + "Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)", + root, + model, + nameof(SettingsModel.AutoGoHomeInterval), + DeprecatedHotkeyGoesHomeKey, + (settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan, + JsonSerializationContext.Default.Boolean); + + return migrated; + } + + private bool TryMigrate(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action apply, JsonTypeInfo jsonTypeInfo) + { + try + { + if (root.ContainsKey(newKey) && root[newKey] is not null) + { + return false; + } + + if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null) + { + var value = oldNode.Deserialize(jsonTypeInfo); + apply(model, value!); + return true; + } + } + catch (Exception ex) + { + Log_MigrationFailure(migrationName, ex); + } + + return false; + } + + [LoggerMessage(Level = LogLevel.Error, Message = "Settings migration '{MigrationName}' failed.")] + partial void Log_MigrationFailure(string MigrationName, Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Settings migration check failed.")] + partial void Log_MigrationCheckFailure(Exception exception); +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/TrayIconService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/TrayIconService.cs index e94efd1239..77d880782d 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/TrayIconService.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/TrayIconService.cs @@ -8,19 +8,23 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.CommandPalette.UI.Models; using Microsoft.CommandPalette.UI.Models.Messages; using Microsoft.UI.Xaml; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; using WinRT.Interop; -using RS_ = Microsoft.CommandPalette.UI.Models.Helpers.ResourceLoaderInstance; +using RS_ = Microsoft.CommandPalette.UI.Services.Helpers.ResourceLoaderInstance; namespace Microsoft.CommandPalette.UI.Services; [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_*")] [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_*")] -public sealed partial class TrayIconService +public sealed partial class TrayIconService : IDisposable { private const uint MY_NOTIFY_ID = 1000; private const uint WM_TRAY_ICON = PInvoke.WM_USER + 1; - private readonly SettingsModel _settingsModel; + private readonly SettingsService _settingsService; private readonly uint WM_TASKBAR_RESTART; private Window? _window; @@ -31,9 +35,10 @@ public sealed partial class TrayIconService private DestroyIconSafeHandle? _largeIcon; private DestroyMenuSafeHandle? _popupMenu; - public TrayIconService(SettingsModel settingsModel) + public TrayIconService(SettingsService settingsService) { - _settingsModel = settingsModel; + _settingsService = settingsService; + _settingsService.SettingsChanged += OnSettingsChanged; // TaskbarCreated is the message that's broadcast when explorer.exe // restarts. We need to know when that happens to be able to bring our @@ -41,9 +46,14 @@ public sealed partial class TrayIconService WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated"); } - public void SetupTrayIcon(bool? showSystemTrayIcon = null) + private void OnSettingsChanged(SettingsModel sender, object? args) { - if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon) + SetupTrayIcon(); + } + + public void SetupTrayIcon() + { + if (_settingsService.CurrentSettings.ShowSystemTrayIcon) { if (_window is null) { @@ -205,4 +215,14 @@ public sealed partial class TrayIconService return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam); } + + public void Dispose() + { + if (_settingsService is not null) + { + _settingsService.SettingsChanged -= OnSettingsChanged; + } + + Destroy(); + } } diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj index 5580976379..020582dc91 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj @@ -24,6 +24,7 @@ + \ No newline at end of file diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.cs index e3adadbd1c..032316b01d 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.cs @@ -4,11 +4,11 @@ using System.ComponentModel; using Microsoft.CommandPalette.UI.Models; -using Microsoft.Extensions.Logging; +using Microsoft.CommandPalette.UI.Services; namespace Microsoft.CommandPalette.UI.ViewModels.Settings; -public partial class SettingsViewModel : INotifyPropertyChanged +public partial class SettingsViewModel : INotifyPropertyChanged, IDisposable { private static readonly List AutoGoHomeIntervals = [ @@ -23,18 +23,17 @@ public partial class SettingsViewModel : INotifyPropertyChanged TimeSpan.FromSeconds(180), ]; - private readonly ILogger logger; - private readonly SettingsModel _settings; - private readonly IServiceProvider _serviceProvider; + private readonly SettingsService _settingsService; + private SettingsModel _settingsModel; public event PropertyChangedEventHandler? PropertyChanged; public HotkeySettings? Hotkey { - get => _settings.Hotkey; + get => _settingsModel.Hotkey; set { - _settings.Hotkey = value ?? SettingsModel.DefaultActivationShortcut; + _settingsModel.Hotkey = value ?? SettingsModel.DefaultActivationShortcut; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey))); Save(); } @@ -42,10 +41,10 @@ public partial class SettingsViewModel : INotifyPropertyChanged public bool UseLowLevelGlobalHotkey { - get => _settings.UseLowLevelGlobalHotkey; + get => _settingsModel.UseLowLevelGlobalHotkey; set { - _settings.UseLowLevelGlobalHotkey = value; + _settingsModel.UseLowLevelGlobalHotkey = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey))); Save(); } @@ -53,90 +52,90 @@ public partial class SettingsViewModel : INotifyPropertyChanged public bool AllowExternalReload { - get => _settings.AllowExternalReload; + get => _settingsModel.AllowExternalReload; set { - _settings.AllowExternalReload = value; + _settingsModel.AllowExternalReload = value; Save(); } } public bool ShowAppDetails { - get => _settings.ShowAppDetails; + get => _settingsModel.ShowAppDetails; set { - _settings.ShowAppDetails = value; + _settingsModel.ShowAppDetails = value; Save(); } } public bool BackspaceGoesBack { - get => _settings.BackspaceGoesBack; + get => _settingsModel.BackspaceGoesBack; set { - _settings.BackspaceGoesBack = value; + _settingsModel.BackspaceGoesBack = value; Save(); } } public bool SingleClickActivates { - get => _settings.SingleClickActivates; + get => _settingsModel.SingleClickActivates; set { - _settings.SingleClickActivates = value; + _settingsModel.SingleClickActivates = value; Save(); } } public bool HighlightSearchOnActivate { - get => _settings.HighlightSearchOnActivate; + get => _settingsModel.HighlightSearchOnActivate; set { - _settings.HighlightSearchOnActivate = value; + _settingsModel.HighlightSearchOnActivate = value; Save(); } } public int MonitorPositionIndex { - get => (int)_settings.SummonOn; + get => (int)_settingsModel.SummonOn; set { - _settings.SummonOn = (MonitorBehavior)value; + _settingsModel.SummonOn = (MonitorBehavior)value; Save(); } } public bool ShowSystemTrayIcon { - get => _settings.ShowSystemTrayIcon; + get => _settingsModel.ShowSystemTrayIcon; set { - _settings.ShowSystemTrayIcon = value; + _settingsModel.ShowSystemTrayIcon = value; Save(); } } public bool IgnoreShortcutWhenFullscreen { - get => _settings.IgnoreShortcutWhenFullscreen; + get => _settingsModel.IgnoreShortcutWhenFullscreen; set { - _settings.IgnoreShortcutWhenFullscreen = value; + _settingsModel.IgnoreShortcutWhenFullscreen = value; Save(); } } public bool DisableAnimations { - get => _settings.DisableAnimations; + get => _settingsModel.DisableAnimations; set { - _settings.DisableAnimations = value; + _settingsModel.DisableAnimations = value; Save(); } } @@ -145,7 +144,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged { get { - var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval); + var index = AutoGoHomeIntervals.IndexOf(_settingsModel.AutoGoHomeInterval); return index >= 0 ? index : 0; } @@ -153,7 +152,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged { if (value >= 0 && value < AutoGoHomeIntervals.Count) { - _settings.AutoGoHomeInterval = AutoGoHomeIntervals[value]; + _settingsModel.AutoGoHomeInterval = AutoGoHomeIntervals[value]; } Save(); @@ -162,11 +161,12 @@ public partial class SettingsViewModel : INotifyPropertyChanged // public ObservableCollection CommandProviders { get; } = []; // public SettingsExtensionsViewModel Extensions { get; } - public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler, ILogger logger) + public SettingsViewModel(SettingsService settingsService) { - _settings = settings; - _serviceProvider = serviceProvider; - this.logger = logger; + _settingsService = settingsService; + _settingsModel = _settingsService.CurrentSettings; + + _settingsService.SettingsChanged += OnSettingsChanged; // var activeProviders = GetCommandProviders(); // var allProviderSettings = _settings.ProviderSettings; @@ -179,11 +179,27 @@ public partial class SettingsViewModel : INotifyPropertyChanged // Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler); } + private void OnSettingsChanged(SettingsModel sender, object? args) + { + _settingsModel = sender; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); + } + // private IEnumerable GetCommandProviders() // { // var manager = _serviceProvider.GetService()!; // var allProviders = manager.CommandProviders; // return allProviders; // } - private void Save() => SettingsModel.SaveSettings(_settings, logger); + private void Save() => _settingsService.SaveSettings(_settingsModel); + + public void Dispose() + { + if (_settingsService is not null) + { + _settingsService.SettingsChanged -= OnSettingsChanged; + } + + GC.SuppressFinalize(this); + } } diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs index e5d1a58b5b..ee34e74614 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs @@ -2,9 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CommandPalette.UI.Helpers; -using Microsoft.CommandPalette.UI.Models; using Microsoft.CommandPalette.UI.Pages; using Microsoft.CommandPalette.UI.Services; using Microsoft.CommandPalette.ViewModels; @@ -22,6 +20,7 @@ public partial class App : Application { private readonly ILogger logger; private readonly GlobalErrorHandler _globalErrorHandler; + private readonly IServiceProvider _services; /// /// Gets the current instance in use. @@ -32,10 +31,6 @@ public partial class App : Application public ETWTrace EtwTrace { get; private set; } = new ETWTrace(); - /// - /// Gets the instance to resolve application services. - /// - public IServiceProvider Services { get; } public App(ILogger logger) { @@ -46,7 +41,7 @@ public partial class App : Application _globalErrorHandler.Register(this); #endif - Services = ConfigureServices(); + _services = ConfigureServices(); this.InitializeComponent(); @@ -70,12 +65,10 @@ public partial class App : Application services.AddSingleton(logger); services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext()); - // Register settings & app state - var settingsModel = SettingsModel.LoadSettings(logger); - services.AddSingleton(settingsModel); - - var appStateModel = AppStateModel.LoadState(logger); - services.AddSingleton(appStateModel); + // Register settings & app state services first + // because other services depend on them + services.AddSingleton(); + services.AddSingleton(); // Register services services.AddSingleton(); @@ -99,7 +92,7 @@ public partial class App : Application { var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs(); - var mainWindow = Services.GetRequiredService(); + var mainWindow = _services.GetRequiredService(); AppWindow = mainWindow; ((MainWindow)AppWindow).HandleLaunchNonUI(activatedEventArgs); diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Converters/PlaceholderTextConverter.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Converters/PlaceholderTextConverter.cs index ad0af40afe..2be839cd83 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Converters/PlaceholderTextConverter.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Converters/PlaceholderTextConverter.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.UI.Xaml.Data; -using RS_ = Microsoft.CommandPalette.UI.Helpers.ResourceLoaderInstance; +using RS_ = Microsoft.CommandPalette.UI.Services.Helpers.ResourceLoaderInstance; namespace Microsoft.CommandPalette.UI.Converters; diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Helpers/GlobalErrorHandler.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Helpers/GlobalErrorHandler.cs index a2c405c288..dbd6c0035d 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Helpers/GlobalErrorHandler.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Helpers/GlobalErrorHandler.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.CommandPalette.UI.Services; +using Microsoft.CommandPalette.UI.Services.Helpers; using Microsoft.Extensions.Logging; using Windows.Win32; using Windows.Win32.Foundation; diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs index d58d79bdff..87f62534cd 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs @@ -11,6 +11,7 @@ using Microsoft.CommandPalette.UI.Models; using Microsoft.CommandPalette.UI.Models.Events; using Microsoft.CommandPalette.UI.Models.Messages; using Microsoft.CommandPalette.UI.Pages; +using Microsoft.CommandPalette.UI.Services; using Microsoft.CommandPalette.UI.ViewModels.Helpers; using Microsoft.Extensions.Logging; using Microsoft.PowerToys.Telemetry; @@ -35,7 +36,7 @@ using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.WindowsAndMessaging; using WinRT; using WinUIEx; -using RS_ = Microsoft.CommandPalette.UI.Helpers.ResourceLoaderInstance; +using RS_ = Microsoft.CommandPalette.UI.Services.Helpers.ResourceLoaderInstance; namespace Microsoft.CommandPalette.UI; @@ -47,7 +48,7 @@ public sealed partial class MainWindow : WindowEx, { private readonly ILogger logger; private readonly ShellPage _shellPage; - private readonly SettingsModel _settingsModel; + private readonly SettingsService _settingsService; private readonly TrayIconService _trayIconService; private const int DefaultWidth = 800; private const int DefaultHeight = 480; @@ -72,13 +73,13 @@ public sealed partial class MainWindow : WindowEx, private WindowPosition _currentWindowPosition = new(); - public MainWindow(ShellPage shellPage, SettingsModel settingsModel, TrayIconService trayIconService, ILogger logger) + public MainWindow(ShellPage shellPage, SettingsService settingsService, TrayIconService trayIconService, ILogger logger) { InitializeComponent(); this.logger = logger; _shellPage = shellPage; - _settingsModel = settingsModel; + _settingsService = settingsService; _trayIconService = trayIconService; RootElement.Children.Add(_shellPage); @@ -128,9 +129,10 @@ public sealed partial class MainWindow : WindowEx, var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc); _originalWndProc = Marshal.GetDelegateForFunctionPointer(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer)); - // Load our settings, and then also wire up a settings changed handler - HotReloadSettings(); - _settingsModel.SettingsChanged += SettingsChangedHandler; + // Wire up a settings changed handler + _settingsService.SettingsChanged += SettingsChangedHandler; + + _trayIconService.SetupTrayIcon(); // Make sure that we update the acrylic theme when the OS theme changes RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic); @@ -150,7 +152,7 @@ public sealed partial class MainWindow : WindowEx, public void Receive(ShowWindowMessage message) { - ShowHwnd(message.Hwnd, _settingsModel.SummonOn); + ShowHwnd(message.Hwnd, _settingsService.CurrentSettings.SummonOn); } public void Receive(HideWindowMessage message) @@ -217,7 +219,7 @@ public sealed partial class MainWindow : WindowEx, private void RestoreWindowPosition() { - if (_settingsModel.LastWindowPosition is not WindowPosition savedPosition) + if (_settingsService.CurrentSettings.LastWindowPosition is not WindowPosition savedPosition) { PositionCentered(); return; @@ -264,12 +266,11 @@ public sealed partial class MainWindow : WindowEx, private void HotReloadSettings() { - SetupHotkey(_settingsModel); - _trayIconService.SetupTrayIcon(_settingsModel.ShowSystemTrayIcon); + SetupHotkey(_settingsService.CurrentSettings); - _ignoreHotKeyWhenFullScreen = _settingsModel.IgnoreShortcutWhenFullscreen; + _ignoreHotKeyWhenFullScreen = _settingsService.CurrentSettings.IgnoreShortcutWhenFullscreen; - _autoGoHomeInterval = _settingsModel.AutoGoHomeInterval; + _autoGoHomeInterval = _settingsService.CurrentSettings.AutoGoHomeInterval; _autoGoHomeTimer.Interval = _autoGoHomeInterval; } @@ -614,9 +615,10 @@ public sealed partial class MainWindow : WindowEx, { UpdateWindowPositionInMemory(); - if (_settingsModel is not null) + if (_settingsService is not null) { - _settingsModel.LastWindowPosition = new WindowPosition + var settings = _settingsService.CurrentSettings; + settings.LastWindowPosition = new WindowPosition { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y, @@ -627,12 +629,12 @@ public sealed partial class MainWindow : WindowEx, ScreenHeight = _currentWindowPosition.ScreenHeight, }; - SettingsModel.SaveSettings(_settingsModel, logger); + _settingsService.SaveSettings(settings); } // var extensionService = serviceProvider.GetService()!; // extensionService.SignalStopExtensionsAsync(); - _trayIconService.Destroy(); + // _trayIconService.Destroy(); // WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592). // Workaround by turning it off before shutdown. @@ -770,7 +772,7 @@ public sealed partial class MainWindow : WindowEx, } else if (uri.StartsWith("x-cmdpal://reload", StringComparison.OrdinalIgnoreCase)) { - if (_settingsModel.AllowExternalReload == true) + if (_settingsService.CurrentSettings.AllowExternalReload == true) { Log_ExternalReloadTriggered(); WeakReferenceMessenger.Default.Send(new()); diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj index 94978a2862..9f188f551d 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj @@ -161,7 +161,7 @@ - + diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.cs index c1d0ed5fdf..25edc14a66 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.cs @@ -4,8 +4,8 @@ using System.Text; using CommunityToolkit.Mvvm.Messaging; -using Microsoft.CommandPalette.UI.Helpers; using Microsoft.CommandPalette.UI.Models; +using Microsoft.CommandPalette.UI.Models.Helpers; using Microsoft.CommandPalette.UI.Models.Messages; using Microsoft.CommandPalette.UI.ViewModels; using Microsoft.CommandPalette.ViewModels;