mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
[Settings] Implement singleton pattern for ShortcutConflictWindow (#42440)
## Summary Fixes issue where multiple ShortcutConflictWindow instances could be opened simultaneously. The window now follows the same singleton pattern as OobeWindow - only one instance can exist at a time, and attempting to open another brings the existing window to the foreground. ## Changes Implemented singleton management for `ShortcutConflictWindow` following the established pattern used by `OobeWindow`: ### App.xaml.cs - Added static field to store the singleton window instance - Added `GetShortcutConflictWindow()`, `SetShortcutConflictWindow()`, and `ClearShortcutConflictWindow()` methods for lifecycle management ### ShortcutConflictWindow.xaml.cs - Updated `WindowEx_Closed` event handler to call `App.ClearShortcutConflictWindow()` to properly clean up the singleton reference when the window is closed ### Updated all three entry points that create ShortcutConflictWindow: - **ShortcutConflictControl.xaml.cs** (Dashboard conflict warning) - **ShortcutControl.xaml.cs** (Settings page shortcut controls) - **OobeOverview.xaml.cs** (OOBE overview page) Each location now checks if a window already exists using `App.GetShortcutConflictWindow()`: - If no window exists, creates a new one and registers it via `App.SetShortcutConflictWindow()` - If a window already exists, simply calls `Activate()` to bring it to the foreground ## Testing The fix ensures that: - ✅ Only one ShortcutConflictWindow can be open at a time - ✅ Clicking the shortcut conflict button when a window is already open activates the existing window instead of creating a duplicate - ✅ The window reference is properly cleared when closed, allowing a new instance to be created in future interactions Fixes #[issue_number] > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `i1qvsblobprodcus353.vsblob.vsassets.io` > - Triggering command: `dotnet build PowerToys.Settings.csproj --configuration Release` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/microsoft/PowerToys/settings/copilot/coding_agent) (admins only) > > </details> <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>[Settings] Single Shortcuts Conflicts window</issue_title> > <issue_description>### Microsoft PowerToys version > > 0.95.0 > > ### Installation method > > GitHub > > ### Area(s) with issue? > > Settings > > ### Steps to reproduce > > Multiple shortcut conflicts window can be launched. > Should have the same behavior of OOBE window. > If shortcut conflicts window is already opened, pressing the button should bring the window in foreground. > > ### ✔️ Expected Behavior > > Single shortcuts conflicts window > > ### ❌ Actual Behavior > > Multiple shortcut conflicts window can be launched > > ### Additional Information > > _No response_ > > ### Other Software > > _No response_</issue_description> > > <agent_instructions>Settings ShortcutConflictWindow should have the same behavior of OobeWindow. > When ShortcutConflictWindow is already opened, activate that window instead of opening another one. </agent_instructions> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> Fixes microsoft/PowerToys#42437 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: davidegiacometti <25966642+davidegiacometti@users.noreply.github.com> Co-authored-by: Davide Giacometti <davide.giacometti@outlook.it> Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: vanzue <vanzue@outlook.com>
This commit is contained in:
@@ -17,6 +17,7 @@ using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -35,6 +36,8 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
private ScoobeWindow scoobeWindow;
|
||||
|
||||
private ShortcutConflictWindow shortcutConflictWindow;
|
||||
|
||||
private enum Arguments
|
||||
{
|
||||
PTPipeName = 1,
|
||||
@@ -336,10 +339,10 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
return settingsWindow;
|
||||
}
|
||||
|
||||
public static bool IsOobeOrScoobeOpen()
|
||||
public static bool IsSecondaryWindowOpen()
|
||||
{
|
||||
var app = (App)Current;
|
||||
return app.oobeWindow != null || app.scoobeWindow != null;
|
||||
return app.oobeWindow != null || app.scoobeWindow != null || app.shortcutConflictWindow != null;
|
||||
}
|
||||
|
||||
public void OpenScoobe()
|
||||
@@ -384,6 +387,25 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenShortcutConflictWindow()
|
||||
{
|
||||
if (shortcutConflictWindow == null)
|
||||
{
|
||||
shortcutConflictWindow = new ShortcutConflictWindow();
|
||||
|
||||
shortcutConflictWindow.Closed += (_, _) =>
|
||||
{
|
||||
shortcutConflictWindow = null;
|
||||
};
|
||||
|
||||
shortcutConflictWindow.Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowHelpers.BringToForeground(shortcutConflictWindow.GetWindowHandle());
|
||||
}
|
||||
}
|
||||
|
||||
public static Type GetPage(string settingWindow)
|
||||
{
|
||||
switch (settingWindow)
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
|
||||
using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
|
||||
@@ -154,11 +151,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
ConflictCount = this.ConflictCount,
|
||||
});
|
||||
|
||||
// Create and show the new window instead of dialog
|
||||
var conflictWindow = new ShortcutConflictWindow();
|
||||
|
||||
// Show the window
|
||||
conflictWindow.Activate();
|
||||
((App)App.Current)!.OpenShortcutConflictWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
// 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 CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -26,7 +23,11 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
|
||||
|
||||
public ShortcutConflictWindow()
|
||||
{
|
||||
App.ThemeService.ThemeChanged += OnThemeChanged;
|
||||
App.ThemeService.ApplyTheme();
|
||||
|
||||
var settingsUtils = SettingsUtils.Default;
|
||||
|
||||
ViewModel = new ShortcutConflictViewModel(
|
||||
settingsUtils,
|
||||
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
|
||||
@@ -50,6 +51,11 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
|
||||
ViewModel.OnPageLoaded();
|
||||
}
|
||||
|
||||
private void OnThemeChanged(object sender, ElementTheme theme)
|
||||
{
|
||||
WindowHelper.SetTheme(this, theme);
|
||||
}
|
||||
|
||||
private void CenterOnScreen()
|
||||
{
|
||||
var displayArea = DisplayArea.GetFromWindowId(this.AppWindow.Id, DisplayAreaFallback.Nearest);
|
||||
@@ -127,6 +133,14 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
|
||||
private void WindowEx_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
ViewModel?.Dispose();
|
||||
|
||||
var mainWindow = App.GetSettingsWindow();
|
||||
if (mainWindow != null)
|
||||
{
|
||||
mainWindow.CloseHiddenWindow();
|
||||
}
|
||||
|
||||
App.ThemeService.ThemeChanged -= OnThemeChanged;
|
||||
}
|
||||
|
||||
private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args)
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.Linq;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
|
||||
@@ -300,9 +299,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
// Close the current shortcut dialog
|
||||
shortcutDialog.Hide();
|
||||
|
||||
// Create and show the ShortcutConflictWindow
|
||||
var conflictWindow = new ShortcutConflictWindow();
|
||||
conflictWindow.Activate();
|
||||
((App)App.Current)!.OpenShortcutConflictWindow();
|
||||
}
|
||||
|
||||
private void UpdateKeyVisualStyles()
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
var hWnd = WindowNative.GetWindowHandle(this);
|
||||
WindowHelper.SerializePlacement(hWnd);
|
||||
|
||||
if (!App.IsOobeOrScoobeOpen())
|
||||
if (!App.IsSecondaryWindowOpen())
|
||||
{
|
||||
App.ClearSettingsWindow();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
DataContext = ViewModel;
|
||||
|
||||
Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
Unloaded += (s, e) => ViewModel?.Dispose();
|
||||
}
|
||||
|
||||
public void RefreshEnabledState()
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
// Flag to prevent toggle operations during sorting to avoid race conditions.
|
||||
private bool _isSorting;
|
||||
private bool _isDisposed;
|
||||
|
||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||
|
||||
@@ -132,8 +133,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
private void OnSettingsChanged(GeneralSettings newSettings)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dispatcher.TryEnqueue(() =>
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
generalSettingsConfig = newSettings;
|
||||
|
||||
// Update local field and notify UI if sort order changed
|
||||
@@ -149,8 +160,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dispatcher.TryEnqueue(() =>
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var allConflictData = e.Conflicts;
|
||||
foreach (var inAppConflict in allConflictData.InAppConflicts)
|
||||
{
|
||||
@@ -363,6 +384,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
/// </summary>
|
||||
public void ModuleEnabledChangedOnSettingsPage()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if this was triggered by a UI change that we're already handling.
|
||||
if (_isUpdatingFromUI)
|
||||
{
|
||||
@@ -391,6 +417,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
/// </summary>
|
||||
private void RefreshShortcutModules()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dispatcher.HasThreadAccess)
|
||||
{
|
||||
_ = dispatcher.TryEnqueue(DispatcherQueuePriority.Normal, RefreshShortcutModules);
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutModules.Clear();
|
||||
ActionModules.Clear();
|
||||
|
||||
@@ -804,6 +841,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
base.Dispose();
|
||||
if (_settingsRepository != null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user