Compare commits

..

2 Commits

Author SHA1 Message Date
Gordon Lam (SH)
ceb8434f41 chore(prompts): add fix active PR comments prompt with scoped changes 2026-01-23 13:43:15 +08:00
Gordon Lam (SH)
30bbcf32a4 chore(prompts): remove model references from prompt files 2026-01-21 17:29:32 +08:00
33 changed files with 208 additions and 1532 deletions

View File

@@ -1,6 +1,5 @@
---
agent: 'agent'
model: 'GPT-5.1-Codex-Max'
description: 'Generate an 80-character git commit title for the local diff'
---

View File

@@ -1,6 +1,5 @@
---
agent: 'agent'
model: 'GPT-5.1-Codex-Max'
description: 'Generate a PowerToys-ready pull request description from the local diff'
---

View File

@@ -1,6 +1,5 @@
---
agent: 'agent'
model: 'GPT-5.1-Codex-Max'
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
---

View File

@@ -0,0 +1,70 @@
---
description: 'Fix active pull request comments with scoped changes'
name: 'fix-pr-active-comments'
agent: 'agent'
argument-hint: 'PR number or active PR URL'
---
# Fix Active PR Comments
## Mission
Resolve active pull request comments by applying only simple fixes. For complex refactors, write a plan instead of changing code.
## Scope & Preconditions
- You must have an active pull request context or a provided PR number.
- Only implement simple changes. Do not implement large refactors.
- If required context is missing, request it and stop.
## Inputs
- Required: ${input:pr_number:PR number or URL}
- Optional: ${input:comment_scope:files or areas to focus on}
- Optional: ${input:fixing_guidelines:additional fixing guidelines from the user}
## Workflow
1. Locate all active (unresolved) PR review comments for the given PR.
2. For each comment, classify the change scope:
- Simple change: limited edits, localized fix, low risk, no broad redesign.
- Large refactor: multi-file redesign, architecture change, or risky behavior change.
3. For each large refactor request:
- Do not modify code.
- Write a planning document to Generated Files/prReview/${input:pr_number}/fixPlan/.
4. For each simple change request:
- Implement the fix with minimal edits.
- Run quick checks if needed.
- Commit and push the change.
5. For comments that seem invalid, unclear, or not applicable (even if simple):
- Do not change code.
- Add the item to a summary table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
- Consult back to the end user in a friendly, polite tone.
6. Respond to each comment that you fixed:
- Reply in the active conversation.
- Use a polite or friendly tone.
- Keep the response under 200 words.
- Resolve the comment after replying.
## Output Expectations
- Simple fixes: code changes committed and pushed.
- Large refactors: a plan file saved to Generated Files/prReview/${input:pr_number}/fixPlan/.
- Invalid or unclear comments: captured in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
- Each fixed comment has a reply under 200 words and is resolved.
## Plan File Template
Use this template for each large refactor item:
# Fix Plan: <short title>
## Context
- Comment link:
- Impacted areas:
## Overview Table Template
Use this table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md:
| Comment link | Summary | Reason not applied | Suggested follow-up |
| --- | --- | --- | --- |
| | | | |
## Quality Assurance
- Verify plan file path exists.
- Ensure no code changes were made for large refactor items.
- Confirm replies are under 200 words and comments are resolved.

View File

@@ -1,6 +1,5 @@
---
agent: 'agent'
model: 'GPT-5.1-Codex-Max'
description: 'Resolve Code scanning / check-spelling comments on the active PR'
---

View File

@@ -1,6 +1,5 @@
---
agent: 'agent'
model: 'GPT-5.1-Codex-Max'
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
---

View File

@@ -1,6 +1,5 @@
---
agent: 'agent'
model: 'GPT-5.1-Codex-Max'
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
---

View File

@@ -7,62 +7,22 @@
Queries Windows for all connected monitors and saves their configuration
(position, size, DPI, primary status) to a JSON file that can be used
for testing the CursorWrap module.
By default, potentially identifying information (computer name, user name,
device names) is anonymized to protect privacy when sharing layout files.
.PARAMETER OutputPath
Path where the JSON file will be saved. Default: cursorwrap_monitor_layout.json
.PARAMETER AddUserMachineNames
Include computer name and user name in the output. By default these are
blank to protect privacy when sharing layout files.
.PARAMETER AddDeviceNames
Include device names (e.g., \\.\DISPLAY1) in the output. By default these
are anonymized to "DISPLAY1", "DISPLAY2", etc. to reduce fingerprinting.
.PARAMETER Help
Show this help message and exit.
Path where the JSON file will be saved. Default: monitor_layout.json
.EXAMPLE
.\Capture-MonitorLayout.ps1
Captures layout with privacy-safe defaults (no user/machine names).
.EXAMPLE
.\Capture-MonitorLayout.ps1 -OutputPath "my_setup.json"
Saves to a custom filename.
.EXAMPLE
.\Capture-MonitorLayout.ps1 -AddUserMachineNames
Includes computer name and user name in the output.
.EXAMPLE
.\Capture-MonitorLayout.ps1 -AddUserMachineNames -AddDeviceNames
Includes all identifying information (useful for personal debugging).
#>
param(
[Parameter(Mandatory=$false)]
[string]$OutputPath = "cursorwrap_monitor_layout.json",
[Parameter(Mandatory=$false)]
[switch]$AddUserMachineNames,
[Parameter(Mandatory=$false)]
[switch]$AddDeviceNames,
[Parameter(Mandatory=$false)]
[Alias("h", "?")]
[switch]$Help
[string]$OutputPath = "$($env:USERNAME)_monitor_layout.json"
)
# Show help if requested
if ($Help) {
Get-Help $MyInvocation.MyCommand.Path -Detailed
exit 0
}
# Add Windows Forms for screen enumeration
Add-Type -AssemblyName System.Windows.Forms
@@ -177,20 +137,12 @@ function Capture-MonitorLayout {
$screens = [System.Windows.Forms.Screen]::AllScreens
$monitors = @()
$monitorIndex = 1
foreach ($screen in $screens) {
$isPrimary = $screen.Primary
$bounds = $screen.Bounds
$dpi = Get-MonitorDPI -Screen $screen
# Anonymize device name by default to reduce fingerprinting
$deviceName = if ($AddDeviceNames) {
$screen.DeviceName
} else {
"DISPLAY$monitorIndex"
}
$monitor = [ordered]@{
left = $bounds.Left
top = $bounds.Top
@@ -201,11 +153,10 @@ function Capture-MonitorLayout {
dpi = $dpi
scaling_percent = [math]::Round(($dpi / 96.0) * 100, 0)
primary = $isPrimary
device_name = $deviceName
device_name = $screen.DeviceName
}
$monitors += $monitor
$monitorIndex++
# Display info
$primaryTag = if ($isPrimary) { " [PRIMARY]" } else { "" }
@@ -219,11 +170,11 @@ function Capture-MonitorLayout {
Write-Host " Bounds: [$($bounds.Left), $($bounds.Top), $($bounds.Right), $($bounds.Bottom)]"
}
# Create output object with privacy-safe defaults
# Create output object
$output = [ordered]@{
captured_at = (Get-Date -Format "yyyy-MM-ddTHH:mm:sszzz")
computer_name = if ($AddUserMachineNames) { $env:COMPUTERNAME } else { "" }
user_name = if ($AddUserMachineNames) { $env:USERNAME } else { "" }
computer_name = $env:COMPUTERNAME
user_name = $env:USERNAME
monitor_count = $monitors.Count
monitors = $monitors
}
@@ -249,22 +200,10 @@ try {
Write-Host "`n" + ("=" * 80)
Write-Host "SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 80)
if ($layout.computer_name) {
Write-Host "Configuration Name: $($layout.computer_name)"
}
Write-Host "Configuration Name: $($layout.computer_name)"
Write-Host "Captured: $($layout.captured_at)"
Write-Host "Monitors: $($layout.monitor_count)"
# Privacy notice
if (-not $AddUserMachineNames -or -not $AddDeviceNames) {
Write-Host "`nPrivacy: " -NoNewline -ForegroundColor Yellow
$privacyNotes = @()
if (-not $AddUserMachineNames) { $privacyNotes += "user/machine names excluded" }
if (-not $AddDeviceNames) { $privacyNotes += "device names anonymized" }
Write-Host ($privacyNotes -join ", ") -ForegroundColor Yellow
Write-Host " Use -AddUserMachineNames and/or -AddDeviceNames to include." -ForegroundColor DarkGray
}
# Calculate desktop dimensions
$widths = @($layout.monitors | ForEach-Object { $_.width })
$heights = @($layout.monitors | ForEach-Object { $_.height })

View File

@@ -1,259 +0,0 @@
// 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;
}
}
}
}

View File

@@ -1,69 +0,0 @@
// 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();
}
}

View File

@@ -1,62 +0,0 @@
// 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");
}
}

View File

@@ -5,7 +5,6 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -78,12 +77,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private async void Watcher_Changed(object sender, FileSystemEventArgs e)
private 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++)
{
await Task.Delay(100).ConfigureAwait(false);
Thread.Sleep(100);
if (ReloadSettings())
{
SettingsChanged?.Invoke(SettingsConfig);

View File

@@ -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 : ISettingsUtils
public class SettingsUtils
{
public const string DefaultFileName = "settings.json";
private const string DefaultModuleName = "";

View File

@@ -1,21 +0,0 @@
// 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);
}
}

View File

@@ -1,20 +0,0 @@
// 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)
{
}
}
}

View File

@@ -1,21 +0,0 @@
// 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);
}
}

View File

@@ -1,19 +0,0 @@
// 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)
{
}
}
}

View File

@@ -1,21 +0,0 @@
// 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);
}
}

View File

@@ -1,19 +0,0 @@
// 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)
{
}
}
}

View File

@@ -1,19 +0,0 @@
// 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;
}
}
}

View File

@@ -1,21 +0,0 @@
// 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);
}
}

View File

@@ -1,19 +0,0 @@
// 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)
{
}
}
}

View File

@@ -74,9 +74,7 @@
<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" />

View File

@@ -1,131 +0,0 @@
// 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);
}
}

View File

@@ -1,73 +0,0 @@
// 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;
}
}
}

View File

@@ -1,113 +0,0 @@
// 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.");
}
}
}
}

View File

@@ -10,7 +10,6 @@ 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;
@@ -84,9 +83,6 @@ namespace Microsoft.PowerToys.Settings.UI
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
// Initialize dependency injection
InitializeServices();
InitializeComponent();
UnhandledException += App_UnhandledException;
@@ -99,14 +95,6 @@ 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);

View File

@@ -22,19 +22,15 @@ 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;
@@ -47,13 +43,8 @@ namespace Microsoft.PowerToys.Settings.UI
{
placement.ShowCmd = NativeMethods.SW_HIDE;
// Defer full initialization until window is shown
this.Activated += Window_Activated_LazyInit;
}
else
{
// Full initialization for visible windows
CompleteInitialization();
// Restore the last known placement on the first activation
this.Activated += Window_Activated;
}
NativeMethods.SetWindowPlacement(hWnd, ref placement);
@@ -61,16 +52,6 @@ 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 =>
{
@@ -147,10 +128,13 @@ 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)
@@ -166,33 +150,10 @@ namespace Microsoft.PowerToys.Settings.UI
}
}
};
}
private void CompleteInitialization()
{
if (_isFullyInitialized)
{
return;
}
bootTime.Stop();
_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);
}
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
}
private void SetAppTitleBar()
@@ -204,8 +165,6 @@ namespace Microsoft.PowerToys.Settings.UI
public void NavigateToSection(System.Type type)
{
// Ensure full initialization before navigation
CompleteInitialization();
ShellPage.Navigate(type);
}
@@ -263,8 +222,6 @@ namespace Microsoft.PowerToys.Settings.UI
internal void EnsurePageIsSelected()
{
// Ensure full initialization before page selection
CompleteInitialization();
ShellPage.EnsurePageIsSelected();
}
}

View File

@@ -41,9 +41,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
Action stateUpdatingAction = () =>
{
this.DispatcherQueue.TryEnqueue(async () =>
this.DispatcherQueue.TryEnqueue(() =>
{
await ViewModel.RefreshUpdatingStateAsync().ConfigureAwait(true);
ViewModel.RefreshUpdatingState();
});
};
@@ -92,16 +92,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
CheckBugReportStatus();
this.Loaded += (s, e) =>
{
ViewModel.OnPageLoaded();
doRefreshBackupRestoreStatus(100);
// Defer backup status check to after page is loaded with low priority
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
doRefreshBackupRestoreStatus(100);
});
};
this.Loaded += (s, e) => ViewModel.OnPageLoaded();
}
private void OpenColorsSettings_Click(object sender, RoutedEventArgs e)
@@ -130,11 +123,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void RefreshBackupRestoreStatus(int delayMs = 0)
{
Task.Run(async () =>
Task.Run(() =>
{
if (delayMs > 0)
{
await Task.Delay(delayMs).ConfigureAwait(false);
Thread.Sleep(delayMs);
}
var settingsBackupAndRestoreUtils = SettingsBackupAndRestoreUtils.Instance;
@@ -178,7 +171,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private async void ViewDiagnosticData_Click(object sender, RoutedEventArgs e)
{
await ViewModel.ViewDiagnosticDataAsync().ConfigureAwait(false);
await Task.Run(ViewModel.ViewDiagnosticData);
}
private void BugReportToolClicked(object sender, RoutedEventArgs e)

View File

@@ -1,92 +1,43 @@
// 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
{
/// <summary>
/// ViewModel for the File Locksmith settings page.
/// Uses CommunityToolkit.Mvvm for MVVM pattern implementation.
/// </summary>
public partial class FileLocksmithViewModel : ObservableObject
public partial class FileLocksmithViewModel : Observable
{
private GeneralSettings GeneralSettingsConfig { get; set; }
private readonly ISettingsUtils _settingsUtils;
private readonly SettingsUtils _settingsUtils;
private FileLocksmithSettings Settings { get; set; }
private const string ModuleNameConst = FileLocksmithSettings.ModuleName;
private const string ModuleName = 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)_settingsUtils).GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
FileLocksmithLocalProperties localSettings = _settingsUtils.GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
Settings = new FileLocksmithSettings(localSettings);
}
catch (Exception)
@@ -97,12 +48,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
InitializeEnabledValue();
EnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
_fileLocksmithEnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
}
public string GetSettingsSubPath()
{
return _settingsConfigFileFolder + "\\" + ModuleNameConst;
return _settingsConfigFileFolder + "\\" + ModuleName;
}
private void InitializeEnabledValue()
@@ -111,40 +66,65 @@ 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;
}
}
/// <summary>
/// Gets a value indicating whether the enabled state is configured by GPO.
/// </summary>
public bool IsEnabledGpoConfigured => EnabledStateIsGPOConfigured;
partial void OnIsFileLocksmithEnabledChanged(bool value)
public bool IsFileLocksmithEnabled
{
if (EnabledStateIsGPOConfigured)
get => _isFileLocksmithEnabled;
set
{
// If it's GPO configured, shouldn't be able to change this state.
return;
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();
}
}
GeneralSettingsConfig.Enabled.FileLocksmith = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
NotifySettingsChanged();
}
partial void OnEnabledOnContextExtendedMenuChanged(bool value)
public bool EnabledOnContextExtendedMenu
{
Settings.Properties.ExtendedContextMenuOnly.Value = value;
NotifySettingsChanged();
get
{
return _fileLocksmithEnabledOnContextExtendedMenu;
}
set
{
if (value != _fileLocksmithEnabledOnContextExtendedMenu)
{
_fileLocksmithEnabledOnContextExtendedMenu = value;
Settings.Properties.ExtendedContextMenuOnly.Value = value;
OnPropertyChanged(nameof(EnabledOnContextExtendedMenu));
NotifySettingsChanged();
}
}
}
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}
private void NotifySettingsChanged()
@@ -158,10 +138,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.FileLocksmithSettings)));
}
/// <summary>
/// Refreshes the enabled state by re-reading GPO configuration.
/// </summary>
[RelayCommand]
private Func<string, int> SendConfigMSG { get; }
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private bool _isFileLocksmithEnabled;
private bool _fileLocksmithEnabledOnContextExtendedMenu;
public void RefreshEnabledState()
{
InitializeEnabledValue();

View File

@@ -213,20 +213,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_fileWatcher = Helper.GetFileWatcher(string.Empty, UpdatingSettings.SettingsFile, dispatcherAction);
}
// 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
// Diagnostic data retention policy
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
@@ -1362,54 +1356,52 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
NotifyAllBackupAndRestoreProperties();
}
public async Task RefreshUpdatingStateAsync()
public void RefreshUpdatingState()
{
// Load settings with retry on background thread
var config = await Task.Run(async () =>
object oLock = new object();
lock (oLock)
{
var loadedConfig = UpdatingSettings.LoadSettings();
var config = UpdatingSettings.LoadSettings();
// Retry loading if failed
for (int i = 0; i < 3 && loadedConfig == null; i++)
for (int i = 0; i < 3 && config == null; i++)
{
await Task.Delay(100).ConfigureAwait(false);
loadedConfig = UpdatingSettings.LoadSettings();
System.Threading.Thread.Sleep(100);
config = UpdatingSettings.LoadSettings();
}
return loadedConfig;
}).ConfigureAwait(true);
if (config == null)
{
return;
}
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));
}
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()
@@ -1497,7 +1489,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
internal async Task ViewDiagnosticDataAsync()
internal void ViewDiagnosticData()
{
string localLowEtwDirPath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw");
string etwDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\etw");
@@ -1530,7 +1522,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
string tracerptPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "system32");
ETLConverter converter = new ETLConverter(etwDirPath, tracerptPath);
await converter.ConvertDiagnosticsETLsAsync().ConfigureAwait(false);
Task.Run(() => converter.ConvertDiagnosticsETLsAsync()).Wait();
if (Directory.Exists(etwDirPath))
{

View File

@@ -1,242 +0,0 @@
// 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);
}
}
}

View File

@@ -1,108 +0,0 @@
// 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;
}
}
}
}