mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-23 04:27:14 +01:00
Compare commits
1 Commits
main
...
dev/crutka
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8090a990f5 |
259
src/settings-ui/Settings.UI.Library/AsyncSettingsRepository`1.cs
Normal file
259
src/settings-ui/Settings.UI.Library/AsyncSettingsRepository`1.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Async settings repository implementation with caching and thread-safe access.
|
||||
/// Provides non-blocking settings operations suitable for UI applications.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
public sealed class AsyncSettingsRepository<T> : IAsyncSettingsRepository<T>, IDisposable
|
||||
where T : class, ISettingsConfig, new()
|
||||
{
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly string _moduleName;
|
||||
private readonly string _fileName;
|
||||
|
||||
private T _cachedSettings;
|
||||
private FileSystemWatcher _watcher;
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<T> SettingsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncSettingsRepository{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="settingsUtils">The settings utilities instance.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
public AsyncSettingsRepository(ISettingsUtils settingsUtils, string fileName = "settings.json")
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
_fileName = fileName;
|
||||
|
||||
// Get module name from type
|
||||
var settingsItem = new T();
|
||||
_moduleName = settingsItem.GetModuleName();
|
||||
|
||||
InitializeWatcher();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T SettingsConfig
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cachedSettings == null)
|
||||
{
|
||||
_semaphore.Wait();
|
||||
try
|
||||
{
|
||||
if (_cachedSettings == null)
|
||||
{
|
||||
_cachedSettings = LoadSettingsInternal();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
return _cachedSettings;
|
||||
}
|
||||
|
||||
private set => _cachedSettings = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<T> GetSettingsAsync(bool forceRefresh = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!forceRefresh && _cachedSettings != null)
|
||||
{
|
||||
return _cachedSettings;
|
||||
}
|
||||
|
||||
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!forceRefresh && _cachedSettings != null)
|
||||
{
|
||||
return _cachedSettings;
|
||||
}
|
||||
|
||||
_cachedSettings = await Task.Run(() => LoadSettingsInternal(), cancellationToken).ConfigureAwait(false);
|
||||
return _cachedSettings;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SaveSettingsAsync(T settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Temporarily stop watching to avoid self-triggered events
|
||||
StopWatching();
|
||||
|
||||
await Task.Run(
|
||||
() => _settingsUtils.SaveSettings(settings.ToJsonString(), _moduleName, _fileName),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_cachedSettings = settings;
|
||||
}
|
||||
finally
|
||||
{
|
||||
StartWatching();
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ReloadSettings()
|
||||
{
|
||||
_semaphore.Wait();
|
||||
try
|
||||
{
|
||||
var newSettings = LoadSettingsInternal();
|
||||
if (newSettings != null)
|
||||
{
|
||||
_cachedSettings = newSettings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to reload settings for {_moduleName}", ex);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> ReloadSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var newSettings = await Task.Run(() => LoadSettingsInternal(), cancellationToken).ConfigureAwait(false);
|
||||
if (newSettings != null)
|
||||
{
|
||||
_cachedSettings = newSettings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to reload settings for {_moduleName}", ex);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void StopWatching()
|
||||
{
|
||||
if (_watcher != null)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void StartWatching()
|
||||
{
|
||||
if (_watcher != null)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
}
|
||||
|
||||
private T LoadSettingsInternal()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _settingsUtils.GetSettingsOrDefault<T>(_moduleName, _fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load settings for {_moduleName}", ex);
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeWatcher()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = _settingsUtils.GetSettingsFilePath(_moduleName, _fileName);
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
_watcher = new FileSystemWatcher(directory, fileName);
|
||||
_watcher.NotifyFilter = NotifyFilters.LastWrite;
|
||||
_watcher.Changed += OnWatcherChanged;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to initialize settings watcher for {typeof(T).Name}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnWatcherChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
// Wait a bit for the file write to complete and retry if needed
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
if (await ReloadSettingsAsync().ConfigureAwait(false))
|
||||
{
|
||||
SettingsChanged?.Invoke(_cachedSettings);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_watcher?.Dispose();
|
||||
_semaphore?.Dispose();
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for asynchronous settings repository operations.
|
||||
/// Provides non-blocking access to settings with caching support.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
public interface IAsyncSettingsRepository<T>
|
||||
where T : class, ISettingsConfig, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the settings have been externally changed (e.g., by another process).
|
||||
/// </summary>
|
||||
event Action<T> SettingsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current cached settings synchronously.
|
||||
/// Returns the cached value immediately, or loads if not cached.
|
||||
/// </summary>
|
||||
T SettingsConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the settings asynchronously with optional refresh from disk.
|
||||
/// </summary>
|
||||
/// <param name="forceRefresh">If true, bypasses cache and reads from disk.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The settings object.</returns>
|
||||
ValueTask<T> GetSettingsAsync(bool forceRefresh = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the settings asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to save.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task representing the save operation.</returns>
|
||||
ValueTask SaveSettingsAsync(T settings, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the settings from disk, updating the cache.
|
||||
/// </summary>
|
||||
/// <returns>True if the reload was successful.</returns>
|
||||
bool ReloadSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the settings from disk asynchronously, updating the cache.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>True if the reload was successful.</returns>
|
||||
ValueTask<bool> ReloadSettingsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Stops watching for external file changes.
|
||||
/// </summary>
|
||||
void StopWatching();
|
||||
|
||||
/// <summary>
|
||||
/// Starts watching for external file changes.
|
||||
/// </summary>
|
||||
void StartWatching();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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.PowerToys.Settings.UI.Library.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for settings utility operations to enable dependency injection and testability.
|
||||
/// </summary>
|
||||
public interface ISettingsUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a settings file exists.
|
||||
/// </summary>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>True if the settings file exists.</returns>
|
||||
bool SettingsExists(string powertoy = "", string fileName = "settings.json");
|
||||
|
||||
/// <summary>
|
||||
/// Deletes settings for the specified module.
|
||||
/// </summary>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
void DeleteSettings(string powertoy = "");
|
||||
|
||||
/// <summary>
|
||||
/// Gets settings for the specified module.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>The deserialized settings object.</returns>
|
||||
T GetSettings<T>(string powertoy = "", string fileName = "settings.json")
|
||||
where T : ISettingsConfig, new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets settings for the specified module, or returns default if not found.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>The deserialized settings object or default.</returns>
|
||||
T GetSettingsOrDefault<T>(string powertoy = "", string fileName = "settings.json")
|
||||
where T : ISettingsConfig, new();
|
||||
|
||||
/// <summary>
|
||||
/// Saves settings to a JSON file.
|
||||
/// </summary>
|
||||
/// <param name="jsonSettings">The JSON settings string.</param>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
void SaveSettings(string jsonSettings, string powertoy = "", string fileName = "settings.json");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file path to the settings file.
|
||||
/// </summary>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>The full path to the settings file.</returns>
|
||||
string GetSettingsFilePath(string powertoy = "", string fileName = "settings.json");
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
@@ -77,12 +78,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
}
|
||||
|
||||
private void Watcher_Changed(object sender, FileSystemEventArgs e)
|
||||
private async void Watcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
// Wait a bit for the file write to complete and retry if needed
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
if (ReloadSettings())
|
||||
{
|
||||
SettingsChanged?.Invoke(SettingsConfig);
|
||||
|
||||
@@ -16,7 +16,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
// Some functions are marked as virtual to allow mocking in unit tests.
|
||||
public class SettingsUtils
|
||||
public class SettingsUtils : ISettingsUtils
|
||||
{
|
||||
public const string DefaultFileName = "settings.json";
|
||||
private const string DefaultModuleName = "";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when backup/restore operations complete.
|
||||
/// </summary>
|
||||
public sealed class BackupRestoreCompletedMessage : ValueChangedMessage<BackupRestoreCompletedMessage.ResultData>
|
||||
{
|
||||
public BackupRestoreCompletedMessage(bool success, string message, bool isBackup)
|
||||
: base(new ResultData(success, message, isBackup))
|
||||
{
|
||||
}
|
||||
|
||||
public record ResultData(bool Success, string Message, bool IsBackup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when general settings are updated.
|
||||
/// </summary>
|
||||
public sealed class GeneralSettingsUpdatedMessage : ValueChangedMessage<GeneralSettings>
|
||||
{
|
||||
public GeneralSettingsUpdatedMessage(GeneralSettings settings)
|
||||
: base(settings)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when hotkey conflicts are detected.
|
||||
/// </summary>
|
||||
public sealed class HotkeyConflictDetectedMessage : ValueChangedMessage<HotkeyConflictDetectedMessage.ConflictData>
|
||||
{
|
||||
public HotkeyConflictDetectedMessage(string moduleName, string hotkeyDescription, bool isSystemConflict)
|
||||
: base(new ConflictData(moduleName, hotkeyDescription, isSystemConflict))
|
||||
{
|
||||
}
|
||||
|
||||
public record ConflictData(string ModuleName, string HotkeyDescription, bool IsSystemConflict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when an IPC message is received from the PowerToys runner.
|
||||
/// </summary>
|
||||
public sealed class IPCMessageReceivedMessage : ValueChangedMessage<string>
|
||||
{
|
||||
public IPCMessageReceivedMessage(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when a module's enabled state changes.
|
||||
/// </summary>
|
||||
public sealed class ModuleEnabledChangedMessage : ValueChangedMessage<ModuleEnabledChangedMessage.ModuleStateData>
|
||||
{
|
||||
public ModuleEnabledChangedMessage(string moduleName, bool isEnabled)
|
||||
: base(new ModuleStateData(moduleName, isEnabled))
|
||||
{
|
||||
}
|
||||
|
||||
public record ModuleStateData(string ModuleName, bool IsEnabled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent to request navigation to a specific page.
|
||||
/// </summary>
|
||||
public sealed class NavigateToPageMessage : ValueChangedMessage<System.Type>
|
||||
{
|
||||
public NavigateToPageMessage(System.Type pageType)
|
||||
: base(pageType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent to request a restart of PowerToys.
|
||||
/// </summary>
|
||||
public sealed class RestartRequestedMessage
|
||||
{
|
||||
public bool MaintainElevation { get; }
|
||||
|
||||
public RestartRequestedMessage(bool maintainElevation = true)
|
||||
{
|
||||
MaintainElevation = maintainElevation;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/settings-ui/Settings.UI/Messages/SettingsSavedMessage.cs
Normal file
21
src/settings-ui/Settings.UI/Messages/SettingsSavedMessage.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when settings are saved.
|
||||
/// </summary>
|
||||
public sealed class SettingsSavedMessage : ValueChangedMessage<SettingsSavedMessage.SettingsSaveData>
|
||||
{
|
||||
public SettingsSavedMessage(string moduleName)
|
||||
: base(new SettingsSaveData(moduleName))
|
||||
{
|
||||
}
|
||||
|
||||
public record SettingsSaveData(string ModuleName);
|
||||
}
|
||||
}
|
||||
19
src/settings-ui/Settings.UI/Messages/ThemeChangedMessage.cs
Normal file
19
src/settings-ui/Settings.UI/Messages/ThemeChangedMessage.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when the application theme changes.
|
||||
/// </summary>
|
||||
public sealed class ThemeChangedMessage : ValueChangedMessage<string>
|
||||
{
|
||||
public ThemeChangedMessage(string themeName)
|
||||
: base(themeName)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
|
||||
131
src/settings-ui/Settings.UI/Services/INavigationService.cs
Normal file
131
src/settings-ui/Settings.UI/Services/INavigationService.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// 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 Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for navigation services to enable testability and abstraction
|
||||
/// from the static NavigationService class.
|
||||
/// </summary>
|
||||
public interface INavigationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when navigation has completed.
|
||||
/// </summary>
|
||||
event NavigatedEventHandler Navigated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when navigation has failed.
|
||||
/// </summary>
|
||||
event NavigationFailedEventHandler NavigationFailed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the navigation frame.
|
||||
/// </summary>
|
||||
Frame Frame { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether navigation can go back.
|
||||
/// </summary>
|
||||
bool CanGoBack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether navigation can go forward.
|
||||
/// </summary>
|
||||
bool CanGoForward { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigates back in the navigation stack.
|
||||
/// </summary>
|
||||
/// <returns>True if navigation was successful.</returns>
|
||||
bool GoBack();
|
||||
|
||||
/// <summary>
|
||||
/// Navigates forward in the navigation stack.
|
||||
/// </summary>
|
||||
void GoForward();
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the specified page type.
|
||||
/// </summary>
|
||||
/// <param name="pageType">The type of page to navigate to.</param>
|
||||
/// <param name="parameter">Optional navigation parameter.</param>
|
||||
/// <param name="infoOverride">Optional navigation transition override.</param>
|
||||
/// <returns>True if navigation was successful.</returns>
|
||||
bool Navigate(Type pageType, object parameter = null, NavigationTransitionInfo infoOverride = null);
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the specified page type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of page to navigate to.</typeparam>
|
||||
/// <param name="parameter">Optional navigation parameter.</param>
|
||||
/// <param name="infoOverride">Optional navigation transition override.</param>
|
||||
/// <returns>True if navigation was successful.</returns>
|
||||
bool Navigate<T>(object parameter = null, NavigationTransitionInfo infoOverride = null)
|
||||
where T : Page;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a page of the specified type is selected.
|
||||
/// </summary>
|
||||
/// <param name="pageType">The type of page to ensure is selected.</param>
|
||||
void EnsurePageIsSelected(Type pageType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper around the static NavigationService to implement INavigationService.
|
||||
/// Allows for gradual migration and testability.
|
||||
/// </summary>
|
||||
public class NavigationServiceWrapper : INavigationService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event NavigatedEventHandler Navigated
|
||||
{
|
||||
add => NavigationService.Navigated += value;
|
||||
remove => NavigationService.Navigated -= value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event NavigationFailedEventHandler NavigationFailed
|
||||
{
|
||||
add => NavigationService.NavigationFailed += value;
|
||||
remove => NavigationService.NavigationFailed -= value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Frame Frame
|
||||
{
|
||||
get => NavigationService.Frame;
|
||||
set => NavigationService.Frame = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanGoBack => NavigationService.CanGoBack;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanGoForward => NavigationService.CanGoForward;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool GoBack() => NavigationService.GoBack();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void GoForward() => NavigationService.GoForward();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Navigate(Type pageType, object parameter = null, NavigationTransitionInfo infoOverride = null)
|
||||
=> NavigationService.Navigate(pageType, parameter, infoOverride);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Navigate<T>(object parameter = null, NavigationTransitionInfo infoOverride = null)
|
||||
where T : Page
|
||||
=> NavigationService.Navigate<T>(parameter, infoOverride);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void EnsurePageIsSelected(Type pageType) => NavigationService.EnsurePageIsSelected(pageType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for configuring services in the dependency injection container.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds core Settings UI services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddSettingsServices(this IServiceCollection services)
|
||||
{
|
||||
// Register singleton services
|
||||
services.AddSingleton<ISettingsUtils>(SettingsUtils.Default);
|
||||
services.AddSingleton<ThemeService>(App.ThemeService);
|
||||
services.AddSingleton<INavigationService, NavigationServiceWrapper>();
|
||||
|
||||
// Register settings repositories as singletons
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var settingsUtils = sp.GetRequiredService<ISettingsUtils>();
|
||||
return SettingsRepository<GeneralSettings>.GetInstance((SettingsUtils)settingsUtils);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds ViewModel registrations to the service collection.
|
||||
/// ViewModels are registered as transient to create new instances per request.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddViewModels(this IServiceCollection services)
|
||||
{
|
||||
// Register ViewModels as transient for fresh instances
|
||||
// These can be migrated incrementally as each ViewModel is updated
|
||||
|
||||
// Tier 1 ViewModels (low complexity) - to be migrated first
|
||||
// services.AddTransient<FileLocksmithViewModel>();
|
||||
// services.AddTransient<RegistryPreviewViewModel>();
|
||||
// services.AddTransient<CropAndLockViewModel>();
|
||||
|
||||
// Tier 2 ViewModels (medium complexity)
|
||||
// services.AddTransient<ColorPickerViewModel>();
|
||||
// services.AddTransient<AlwaysOnTopViewModel>();
|
||||
// services.AddTransient<PowerOcrViewModel>();
|
||||
// services.AddTransient<HostsViewModel>();
|
||||
|
||||
// Tier 3 ViewModels (medium-high complexity)
|
||||
// services.AddTransient<FancyZonesViewModel>();
|
||||
// services.AddTransient<PowerLauncherViewModel>();
|
||||
// services.AddTransient<KeyboardManagerViewModel>();
|
||||
|
||||
// Tier 4 ViewModels (high complexity) - migrate last
|
||||
// services.AddTransient<GeneralViewModel>();
|
||||
// services.AddTransient<DashboardViewModel>();
|
||||
// services.AddTransient<ShellViewModel>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/settings-ui/Settings.UI/Services/ServiceProvider.cs
Normal file
113
src/settings-ui/Settings.UI/Services/ServiceProvider.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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 Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a static wrapper around <see cref="Microsoft.Extensions.DependencyInjection.ServiceProvider"/>
|
||||
/// to enable gradual migration to dependency injection.
|
||||
/// </summary>
|
||||
public static class ServiceProvider
|
||||
{
|
||||
private static readonly object _lock = new object();
|
||||
private static IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the service provider has been initialized.
|
||||
/// </summary>
|
||||
public static bool IsInitialized => _serviceProvider != null;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the service provider with the specified service collection.
|
||||
/// This should be called once during application startup.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection containing all registered services.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the service provider is already initialized.</exception>
|
||||
public static void Initialize(IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_serviceProvider != null)
|
||||
{
|
||||
throw new InvalidOperationException("ServiceProvider is already initialized.");
|
||||
}
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a service of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to get.</typeparam>
|
||||
/// <returns>The service instance, or null if not registered.</returns>
|
||||
public static T GetService<T>()
|
||||
where T : class
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a required service of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to get.</typeparam>
|
||||
/// <returns>The service instance.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the service is not registered.</exception>
|
||||
public static T GetRequiredService<T>()
|
||||
where T : class
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a service of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The type of service to get.</param>
|
||||
/// <returns>The service instance, or null if not registered.</returns>
|
||||
public static object GetService(Type serviceType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serviceType);
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetService(serviceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a required service of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The type of service to get.</param>
|
||||
/// <returns>The service instance.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the service is not registered.</exception>
|
||||
public static object GetRequiredService(Type serviceType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serviceType);
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetRequiredService(serviceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new scope for scoped services.
|
||||
/// </summary>
|
||||
/// <returns>A new service scope.</returns>
|
||||
public static IServiceScope CreateScope()
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.CreateScope();
|
||||
}
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (_serviceProvider == null)
|
||||
{
|
||||
throw new InvalidOperationException("ServiceProvider has not been initialized. Call Initialize() first.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
|
||||
@@ -83,6 +84,9 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
|
||||
}
|
||||
|
||||
// Initialize dependency injection
|
||||
InitializeServices();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
UnhandledException += App_UnhandledException;
|
||||
@@ -95,6 +99,14 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
});
|
||||
}
|
||||
|
||||
private static void InitializeServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSettingsServices();
|
||||
services.AddViewModels();
|
||||
Services.ServiceProvider.Initialize(services);
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception", e.Exception);
|
||||
|
||||
@@ -22,15 +22,19 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx
|
||||
{
|
||||
private bool _isFullyInitialized;
|
||||
private bool _createHidden;
|
||||
|
||||
public MainWindow(bool createHidden = false)
|
||||
{
|
||||
_createHidden = createHidden;
|
||||
|
||||
var bootTime = new System.Diagnostics.Stopwatch();
|
||||
bootTime.Start();
|
||||
|
||||
this.Activated += Window_Activated_SetIcon;
|
||||
|
||||
App.ThemeService.ThemeChanged += OnThemeChanged;
|
||||
App.ThemeService.ApplyTheme();
|
||||
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
@@ -43,8 +47,13 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
placement.ShowCmd = NativeMethods.SW_HIDE;
|
||||
|
||||
// Restore the last known placement on the first activation
|
||||
this.Activated += Window_Activated;
|
||||
// Defer full initialization until window is shown
|
||||
this.Activated += Window_Activated_LazyInit;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full initialization for visible windows
|
||||
CompleteInitialization();
|
||||
}
|
||||
|
||||
NativeMethods.SetWindowPlacement(hWnd, ref placement);
|
||||
@@ -52,6 +61,16 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title");
|
||||
|
||||
// IPC callbacks must be set up immediately so messages can be received even when hidden
|
||||
SetupIPCCallbacks();
|
||||
|
||||
bootTime.Stop();
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
|
||||
}
|
||||
|
||||
private void SetupIPCCallbacks()
|
||||
{
|
||||
// send IPC Message
|
||||
ShellPage.SetDefaultSndMessageCallback(msg =>
|
||||
{
|
||||
@@ -128,13 +147,10 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
App.GetScoobeWindow().Activate();
|
||||
});
|
||||
|
||||
this.InitializeComponent();
|
||||
SetAppTitleBar();
|
||||
|
||||
// receive IPC Message
|
||||
App.IPCMessageReceivedCallback = (string msg) =>
|
||||
{
|
||||
if (ShellPage.ShellHandler.IPCResponseHandleList != null)
|
||||
if (ShellPage.ShellHandler?.IPCResponseHandleList != null)
|
||||
{
|
||||
var success = JsonObject.TryParse(msg, out JsonObject json);
|
||||
if (success)
|
||||
@@ -150,10 +166,33 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bootTime.Stop();
|
||||
private void CompleteInitialization()
|
||||
{
|
||||
if (_isFullyInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
|
||||
_isFullyInitialized = true;
|
||||
|
||||
App.ThemeService.ApplyTheme();
|
||||
this.InitializeComponent();
|
||||
SetAppTitleBar();
|
||||
}
|
||||
|
||||
private void Window_Activated_LazyInit(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState != WindowActivationState.Deactivated && !_isFullyInitialized)
|
||||
{
|
||||
CompleteInitialization();
|
||||
|
||||
// After lazy init, also restore placement
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
|
||||
NativeMethods.SetWindowPlacement(hWnd, ref placement);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAppTitleBar()
|
||||
@@ -165,6 +204,8 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
public void NavigateToSection(System.Type type)
|
||||
{
|
||||
// Ensure full initialization before navigation
|
||||
CompleteInitialization();
|
||||
ShellPage.Navigate(type);
|
||||
}
|
||||
|
||||
@@ -222,6 +263,8 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
internal void EnsurePageIsSelected()
|
||||
{
|
||||
// Ensure full initialization before page selection
|
||||
CompleteInitialization();
|
||||
ShellPage.EnsurePageIsSelected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
Action stateUpdatingAction = () =>
|
||||
{
|
||||
this.DispatcherQueue.TryEnqueue(() =>
|
||||
this.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
ViewModel.RefreshUpdatingState();
|
||||
await ViewModel.RefreshUpdatingStateAsync().ConfigureAwait(true);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -92,9 +92,16 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
CheckBugReportStatus();
|
||||
|
||||
doRefreshBackupRestoreStatus(100);
|
||||
this.Loaded += (s, e) =>
|
||||
{
|
||||
ViewModel.OnPageLoaded();
|
||||
|
||||
this.Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
// Defer backup status check to after page is loaded with low priority
|
||||
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
doRefreshBackupRestoreStatus(100);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private void OpenColorsSettings_Click(object sender, RoutedEventArgs e)
|
||||
@@ -123,11 +130,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private void RefreshBackupRestoreStatus(int delayMs = 0)
|
||||
{
|
||||
Task.Run(() =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
if (delayMs > 0)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var settingsBackupAndRestoreUtils = SettingsBackupAndRestoreUtils.Instance;
|
||||
@@ -171,7 +178,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private async void ViewDiagnosticData_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(ViewModel.ViewDiagnosticData);
|
||||
await ViewModel.ViewDiagnosticDataAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void BugReportToolClicked(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -1,43 +1,92 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class FileLocksmithViewModel : Observable
|
||||
/// <summary>
|
||||
/// ViewModel for the File Locksmith settings page.
|
||||
/// Uses CommunityToolkit.Mvvm for MVVM pattern implementation.
|
||||
/// </summary>
|
||||
public partial class FileLocksmithViewModel : ObservableObject
|
||||
{
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
|
||||
private FileLocksmithSettings Settings { get; set; }
|
||||
|
||||
private const string ModuleName = FileLocksmithSettings.ModuleName;
|
||||
private const string ModuleNameConst = FileLocksmithSettings.ModuleName;
|
||||
|
||||
private string _settingsConfigFileFolder = string.Empty;
|
||||
|
||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsEnabledGpoConfigured))]
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isFileLocksmithEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _enabledOnContextExtendedMenu;
|
||||
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLocksmithViewModel"/> class
|
||||
/// using dependency injection (for new code).
|
||||
/// </summary>
|
||||
/// <param name="settingsUtils">The settings utilities.</param>
|
||||
/// <param name="generalSettingsRepository">The general settings repository.</param>
|
||||
/// <param name="sendConfigMSG">The IPC message callback.</param>
|
||||
public FileLocksmithViewModel(
|
||||
ISettingsUtils settingsUtils,
|
||||
ISettingsRepository<GeneralSettings> generalSettingsRepository,
|
||||
Func<string, int> sendConfigMSG)
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
ArgumentNullException.ThrowIfNull(generalSettingsRepository);
|
||||
|
||||
GeneralSettingsConfig = generalSettingsRepository.SettingsConfig;
|
||||
SendConfigMSG = sendConfigMSG ?? throw new ArgumentNullException(nameof(sendConfigMSG));
|
||||
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLocksmithViewModel"/> class
|
||||
/// (backward compatible constructor for existing code).
|
||||
/// </summary>
|
||||
public FileLocksmithViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, string configFileSubfolder = "")
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
|
||||
// To obtain the general settings configurations of PowerToys Settings.
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
|
||||
_settingsConfigFileFolder = configFileSubfolder;
|
||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
FileLocksmithLocalProperties localSettings = _settingsUtils.GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
|
||||
FileLocksmithLocalProperties localSettings = ((SettingsUtils)_settingsUtils).GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
|
||||
Settings = new FileLocksmithSettings(localSettings);
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -48,16 +97,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
InitializeEnabledValue();
|
||||
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
_fileLocksmithEnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
|
||||
EnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
|
||||
}
|
||||
|
||||
public string GetSettingsSubPath()
|
||||
{
|
||||
return _settingsConfigFileFolder + "\\" + ModuleName;
|
||||
return _settingsConfigFileFolder + "\\" + ModuleNameConst;
|
||||
}
|
||||
|
||||
private void InitializeEnabledValue()
|
||||
@@ -66,65 +111,40 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
|
||||
{
|
||||
// Get the enabled state from GPO.
|
||||
_enabledStateIsGPOConfigured = true;
|
||||
_isFileLocksmithEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
||||
EnabledStateIsGPOConfigured = true;
|
||||
IsFileLocksmithEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
_isFileLocksmithEnabled = GeneralSettingsConfig.Enabled.FileLocksmith;
|
||||
IsFileLocksmithEnabled = GeneralSettingsConfig.Enabled.FileLocksmith;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFileLocksmithEnabled
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the enabled state is configured by GPO.
|
||||
/// </summary>
|
||||
public bool IsEnabledGpoConfigured => EnabledStateIsGPOConfigured;
|
||||
|
||||
partial void OnIsFileLocksmithEnabledChanged(bool value)
|
||||
{
|
||||
get => _isFileLocksmithEnabled;
|
||||
set
|
||||
if (EnabledStateIsGPOConfigured)
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured)
|
||||
{
|
||||
// If it's GPO configured, shouldn't be able to change this state.
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isFileLocksmithEnabled != value)
|
||||
{
|
||||
_isFileLocksmithEnabled = value;
|
||||
|
||||
GeneralSettingsConfig.Enabled.FileLocksmith = value;
|
||||
OnPropertyChanged(nameof(IsFileLocksmithEnabled));
|
||||
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
|
||||
// TODO: Implement when this module has properties.
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
// If it's GPO configured, shouldn't be able to change this state.
|
||||
return;
|
||||
}
|
||||
|
||||
GeneralSettingsConfig.Enabled.FileLocksmith = value;
|
||||
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
public bool EnabledOnContextExtendedMenu
|
||||
partial void OnEnabledOnContextExtendedMenuChanged(bool value)
|
||||
{
|
||||
get
|
||||
{
|
||||
return _fileLocksmithEnabledOnContextExtendedMenu;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _fileLocksmithEnabledOnContextExtendedMenu)
|
||||
{
|
||||
_fileLocksmithEnabledOnContextExtendedMenu = value;
|
||||
Settings.Properties.ExtendedContextMenuOnly.Value = value;
|
||||
OnPropertyChanged(nameof(EnabledOnContextExtendedMenu));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabledGpoConfigured
|
||||
{
|
||||
get => _enabledStateIsGPOConfigured;
|
||||
Settings.Properties.ExtendedContextMenuOnly.Value = value;
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
@@ -138,13 +158,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.FileLocksmithSettings)));
|
||||
}
|
||||
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
private bool _isFileLocksmithEnabled;
|
||||
private bool _fileLocksmithEnabledOnContextExtendedMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the enabled state by re-reading GPO configuration.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
InitializeEnabledValue();
|
||||
|
||||
@@ -213,14 +213,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
_fileWatcher = Helper.GetFileWatcher(string.Empty, UpdatingSettings.SettingsFile, dispatcherAction);
|
||||
}
|
||||
|
||||
// Diagnostic data retention policy
|
||||
// Defer diagnostic data cleanup to background task to avoid blocking UI
|
||||
_ = Task.Run(CleanupDiagnosticDataAsync);
|
||||
|
||||
InitializeLanguages();
|
||||
}
|
||||
|
||||
private void CleanupDiagnosticDataAsync()
|
||||
{
|
||||
// Diagnostic data retention policy - runs on background thread
|
||||
string etwDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\etw");
|
||||
DeleteDiagnosticDataOlderThan28Days(etwDirPath);
|
||||
|
||||
string localLowEtwDirPath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw");
|
||||
DeleteDiagnosticDataOlderThan28Days(localLowEtwDirPath);
|
||||
|
||||
InitializeLanguages();
|
||||
}
|
||||
|
||||
// Supported languages. Taken from Resources.wxs + default + en-US
|
||||
@@ -1356,52 +1362,54 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
NotifyAllBackupAndRestoreProperties();
|
||||
}
|
||||
|
||||
public void RefreshUpdatingState()
|
||||
public async Task RefreshUpdatingStateAsync()
|
||||
{
|
||||
object oLock = new object();
|
||||
lock (oLock)
|
||||
// Load settings with retry on background thread
|
||||
var config = await Task.Run(async () =>
|
||||
{
|
||||
var config = UpdatingSettings.LoadSettings();
|
||||
var loadedConfig = UpdatingSettings.LoadSettings();
|
||||
|
||||
// Retry loading if failed
|
||||
for (int i = 0; i < 3 && config == null; i++)
|
||||
for (int i = 0; i < 3 && loadedConfig == null; i++)
|
||||
{
|
||||
System.Threading.Thread.Sleep(100);
|
||||
config = UpdatingSettings.LoadSettings();
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
loadedConfig = UpdatingSettings.LoadSettings();
|
||||
}
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return loadedConfig;
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
UpdatingSettingsConfig = config;
|
||||
|
||||
if (PowerToysUpdatingState != config.State)
|
||||
{
|
||||
IsNewVersionDownloading = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool dateChanged = UpdateCheckedDate == UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
bool fileDownloaded = string.IsNullOrEmpty(UpdatingSettingsConfig.DownloadedInstallerFilename);
|
||||
IsNewVersionDownloading = !(dateChanged || fileDownloaded);
|
||||
}
|
||||
|
||||
PowerToysUpdatingState = UpdatingSettingsConfig.State;
|
||||
PowerToysNewAvailableVersion = UpdatingSettingsConfig.NewVersion;
|
||||
PowerToysNewAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
|
||||
UpdateCheckedDate = UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
|
||||
_isNoNetwork = PowerToysUpdatingState == UpdatingSettings.UpdatingState.NetworkError;
|
||||
NotifyPropertyChanged(nameof(IsNoNetwork));
|
||||
NotifyPropertyChanged(nameof(IsNewVersionDownloading));
|
||||
NotifyPropertyChanged(nameof(IsUpdatePanelVisible));
|
||||
_isNewVersionChecked = PowerToysUpdatingState == UpdatingSettings.UpdatingState.UpToDate && !IsNewVersionDownloading;
|
||||
NotifyPropertyChanged(nameof(IsNewVersionCheckedAndUpToDate));
|
||||
|
||||
NotifyPropertyChanged(nameof(IsDownloadAllowed));
|
||||
if (config == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdatingSettingsConfig = config;
|
||||
|
||||
if (PowerToysUpdatingState != config.State)
|
||||
{
|
||||
IsNewVersionDownloading = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool dateChanged = UpdateCheckedDate == UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
bool fileDownloaded = string.IsNullOrEmpty(UpdatingSettingsConfig.DownloadedInstallerFilename);
|
||||
IsNewVersionDownloading = !(dateChanged || fileDownloaded);
|
||||
}
|
||||
|
||||
PowerToysUpdatingState = UpdatingSettingsConfig.State;
|
||||
PowerToysNewAvailableVersion = UpdatingSettingsConfig.NewVersion;
|
||||
PowerToysNewAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
|
||||
UpdateCheckedDate = UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
|
||||
_isNoNetwork = PowerToysUpdatingState == UpdatingSettings.UpdatingState.NetworkError;
|
||||
NotifyPropertyChanged(nameof(IsNoNetwork));
|
||||
NotifyPropertyChanged(nameof(IsNewVersionDownloading));
|
||||
NotifyPropertyChanged(nameof(IsUpdatePanelVisible));
|
||||
_isNewVersionChecked = PowerToysUpdatingState == UpdatingSettings.UpdatingState.UpToDate && !IsNewVersionDownloading;
|
||||
NotifyPropertyChanged(nameof(IsNewVersionCheckedAndUpToDate));
|
||||
|
||||
NotifyPropertyChanged(nameof(IsDownloadAllowed));
|
||||
}
|
||||
|
||||
private void InitializeLanguages()
|
||||
@@ -1489,7 +1497,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal void ViewDiagnosticData()
|
||||
internal async Task ViewDiagnosticDataAsync()
|
||||
{
|
||||
string localLowEtwDirPath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw");
|
||||
string etwDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\etw");
|
||||
@@ -1522,7 +1530,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
string tracerptPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "system32");
|
||||
|
||||
ETLConverter converter = new ETLConverter(etwDirPath, tracerptPath);
|
||||
Task.Run(() => converter.ConvertDiagnosticsETLsAsync()).Wait();
|
||||
await converter.ConvertDiagnosticsETLsAsync().ConfigureAwait(false);
|
||||
|
||||
if (Directory.Exists(etwDirPath))
|
||||
{
|
||||
|
||||
242
src/settings-ui/Settings.UI/ViewModels/ModuleViewModelBase.cs
Normal file
242
src/settings-ui/Settings.UI/ViewModels/ModuleViewModelBase.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for module-specific ViewModels that provides common functionality
|
||||
/// such as enabled state, GPO management, and hotkey conflict handling.
|
||||
/// </summary>
|
||||
public abstract partial class ModuleViewModelBase : ViewModelBase
|
||||
{
|
||||
private readonly Dictionary<string, bool> _hotkeyConflictStatus = new Dictionary<string, bool>();
|
||||
private readonly Dictionary<string, string> _hotkeyConflictTooltips = new Dictionary<string, string>();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsEnabledGpoConfigured))]
|
||||
private bool _isEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isGpoManaged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModuleViewModelBase"/> class.
|
||||
/// </summary>
|
||||
protected ModuleViewModelBase()
|
||||
{
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModuleViewModelBase"/> class with a custom messenger.
|
||||
/// </summary>
|
||||
/// <param name="messenger">The messenger instance to use.</param>
|
||||
protected ModuleViewModelBase(IMessenger messenger)
|
||||
: base(messenger)
|
||||
{
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the module name used for settings and conflict detection.
|
||||
/// </summary>
|
||||
protected abstract string ModuleName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the enabled state is configured by GPO.
|
||||
/// </summary>
|
||||
public bool IsEnabledGpoConfigured => IsGpoManaged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnPageLoaded()
|
||||
{
|
||||
base.OnPageLoaded();
|
||||
Debug.WriteLine($"=== PAGE LOADED: {ModuleName} ===");
|
||||
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all hotkey settings for this module.
|
||||
/// Override in derived classes to return module-specific hotkey settings.
|
||||
/// </summary>
|
||||
/// <returns>A dictionary of module names to hotkey settings arrays.</returns>
|
||||
public virtual Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles updates to hotkey conflicts for the module.
|
||||
/// </summary>
|
||||
protected virtual void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
||||
{
|
||||
UpdateHotkeyConflictStatus(e.Conflicts);
|
||||
var allHotkeySettings = GetAllHotkeySettings();
|
||||
|
||||
void UpdateConflictProperties()
|
||||
{
|
||||
if (allHotkeySettings != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, HotkeySettings[]> kvp in allHotkeySettings)
|
||||
{
|
||||
var module = kvp.Key;
|
||||
var hotkeySettingsList = kvp.Value;
|
||||
|
||||
for (int i = 0; i < hotkeySettingsList.Length; i++)
|
||||
{
|
||||
var key = $"{module.ToLowerInvariant()}_{i}";
|
||||
hotkeySettingsList[i].HasConflict = GetHotkeyConflictStatus(key);
|
||||
hotkeySettingsList[i].ConflictDescription = GetHotkeyConflictTooltip(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsWindow = App.GetSettingsWindow();
|
||||
settingsWindow.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, UpdateConflictProperties);
|
||||
}
|
||||
catch
|
||||
{
|
||||
UpdateConflictProperties();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets module-related conflicts from all conflicts data.
|
||||
/// </summary>
|
||||
protected ModuleConflictsData GetModuleRelatedConflicts(AllHotkeyConflictsData allConflicts)
|
||||
{
|
||||
var moduleConflicts = new ModuleConflictsData();
|
||||
|
||||
if (allConflicts.InAppConflicts != null)
|
||||
{
|
||||
foreach (var conflict in allConflicts.InAppConflicts)
|
||||
{
|
||||
if (IsModuleInvolved(conflict))
|
||||
{
|
||||
moduleConflicts.InAppConflicts.Add(conflict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allConflicts.SystemConflicts != null)
|
||||
{
|
||||
foreach (var conflict in allConflicts.SystemConflicts)
|
||||
{
|
||||
if (IsModuleInvolved(conflict))
|
||||
{
|
||||
moduleConflicts.SystemConflicts.Add(conflict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moduleConflicts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the hotkey conflict status based on all conflicts data.
|
||||
/// </summary>
|
||||
protected virtual void UpdateHotkeyConflictStatus(AllHotkeyConflictsData allConflicts)
|
||||
{
|
||||
_hotkeyConflictStatus.Clear();
|
||||
_hotkeyConflictTooltips.Clear();
|
||||
|
||||
if (allConflicts.InAppConflicts.Count > 0)
|
||||
{
|
||||
foreach (var conflictGroup in allConflicts.InAppConflicts)
|
||||
{
|
||||
foreach (var conflict in conflictGroup.Modules)
|
||||
{
|
||||
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
|
||||
_hotkeyConflictStatus[keyName] = true;
|
||||
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allConflicts.SystemConflicts.Count > 0)
|
||||
{
|
||||
foreach (var conflictGroup in allConflicts.SystemConflicts)
|
||||
{
|
||||
foreach (var conflict in conflictGroup.Modules)
|
||||
{
|
||||
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
|
||||
_hotkeyConflictStatus[keyName] = true;
|
||||
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a specific hotkey has a conflict.
|
||||
/// </summary>
|
||||
protected virtual bool GetHotkeyConflictStatus(string key)
|
||||
{
|
||||
return _hotkeyConflictStatus.ContainsKey(key) && _hotkeyConflictStatus[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conflict tooltip for a specific hotkey.
|
||||
/// </summary>
|
||||
protected virtual string GetHotkeyConflictTooltip(string key)
|
||||
{
|
||||
return _hotkeyConflictTooltips.TryGetValue(key, out string value) ? value : null;
|
||||
}
|
||||
|
||||
private bool IsModuleInvolved(HotkeyConflictGroupData conflict)
|
||||
{
|
||||
if (conflict.Modules == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return conflict.Modules.Any(module =>
|
||||
string.Equals(module.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/settings-ui/Settings.UI/ViewModels/ViewModelBase.cs
Normal file
108
src/settings-ui/Settings.UI/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all ViewModels using CommunityToolkit.Mvvm.
|
||||
/// Provides lifecycle methods and messaging support.
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : ObservableRecipient, IDisposable
|
||||
{
|
||||
private bool _isDisposed;
|
||||
private bool _isActive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
|
||||
/// </summary>
|
||||
protected ViewModelBase()
|
||||
: base(WeakReferenceMessenger.Default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ViewModelBase"/> class with a custom messenger.
|
||||
/// </summary>
|
||||
/// <param name="messenger">The messenger instance to use.</param>
|
||||
protected ViewModelBase(IMessenger messenger)
|
||||
: base(messenger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this ViewModel is currently active (visible).
|
||||
/// </summary>
|
||||
public bool IsViewModelActive => _isActive;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the view associated with this ViewModel is navigated to.
|
||||
/// Override this method to perform initialization when the page is displayed.
|
||||
/// </summary>
|
||||
public virtual void OnNavigatedTo()
|
||||
{
|
||||
_isActive = true;
|
||||
IsActive = true; // Activates message subscriptions in ObservableRecipient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the view associated with this ViewModel is navigated to.
|
||||
/// Override this method to perform async initialization when the page is displayed.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the async operation.</returns>
|
||||
public virtual Task OnNavigatedToAsync()
|
||||
{
|
||||
OnNavigatedTo();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the view associated with this ViewModel is navigated away from.
|
||||
/// Override this method to perform cleanup when the page is no longer displayed.
|
||||
/// </summary>
|
||||
public virtual void OnNavigatedFrom()
|
||||
{
|
||||
_isActive = false;
|
||||
IsActive = false; // Deactivates message subscriptions in ObservableRecipient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the page is loaded. This is triggered from the Page's Loaded event.
|
||||
/// </summary>
|
||||
public virtual void OnPageLoaded()
|
||||
{
|
||||
// Default implementation does nothing.
|
||||
// Override in derived classes to perform actions when the page is loaded.
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">True to release both managed and unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Deactivate message subscriptions
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user