mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
CmdPal: improve full-screen detection (#45891)
## Summary of the Pull Request
This PR improves fullscreen detection for the Command Palette activation
shortcut, adds a separate "busy state" guard, surfaces live notification
state diagnostics in Settings, and provides an opt-in rapid-press
breakthrough to bypass suppression.
The existing fullscreen guard lumped D3D fullscreen, presentation mode,
and the heuristic QUNS_BUSY state into a single check. This made it
impossible to opt into guarding against only true fullscreen while
ignoring false positives from apps like NVIDIA overlay. This PR splits
those concerns, adds diagnostic visibility, and gives users an escape
hatch.
The problem with the detection is that QUNS_RUNNING_D3D_FULL_SCREEN is
intended for exclusive D3D full-screen apps (some games), but it
overlaps with QUNS_BUSY for other games and apps.
- Splits the fullscreen guard into two separate settings
- IsWindowFullscreen() now only checks QUNS_RUNNING_D3D_FULL_SCREEN and
QUNS_PRESENTATION_MODE
- New IsAppBusy() handles the heuristic QUNS_BUSY state separately
- New IgnoreShortcutWhenBusy setting (off by default) so users aren't
silently blocked by false positives
- Migrates from hand-written P/Invoke (NativeMethods.cs, deleted) to
CsWin32-generated bindings
- Adds a live InfoBar in Activation settings when the shortcut is
limited
- Polls SHQueryUserNotificationState every 2 seconds via DispatcherTimer
- Displays a warning describing which state is active (D3D fullscreen,
presentation mode, or busy)
- New GetUserNotificationState() in WindowHelper exposes the raw state
for the UI
- Attributes QUNS_BUSY to known trigger apps
- New FindVisibleTriggerApps() enumerates windows by class name and
process name against a known-app list
- When NVIDIA Overlay (or other known apps) are detected, the InfoBar
message names the likely culprit
- Adds an opt-in rapid-press breakthrough to bypass suppression
- New AllowBreakthroughShortcut setting (off by default)
- Pressing the activation shortcut 3 times within 2 seconds overrides
the guard
- The suppression is automatically bypassed when the Command Palette is
visible - to allow dismissal
## Pictures? Pictures!
<img width="1112" height="769" alt="image"
src="https://github.com/user-attachments/assets/e1d64ace-cfb2-4ba1-a436-3d2d77c18c76"
/>
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #45548
- [x] Closes: #41225
- [x] Closes: #42716
- [x] Closes: #45875
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
1
.github/actions/spell-check/allow/names.txt
vendored
1
.github/actions/spell-check/allow/names.txt
vendored
@@ -223,6 +223,7 @@ Moq
|
||||
mozilla
|
||||
mspaint
|
||||
Newtonsoft
|
||||
NVIDIA
|
||||
onenote
|
||||
openai
|
||||
Quickime
|
||||
|
||||
@@ -31,7 +31,11 @@ public record SettingsModel
|
||||
|
||||
public bool ShowSystemTrayIcon { get; init; } = true;
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen { get; init; }
|
||||
public bool IgnoreShortcutWhenFullscreen { get; init; } = true;
|
||||
|
||||
public bool IgnoreShortcutWhenBusy { get; init; }
|
||||
|
||||
public bool AllowBreakthroughShortcut { get; init; }
|
||||
|
||||
public bool AllowExternalReload { get; init; }
|
||||
|
||||
|
||||
@@ -137,6 +137,24 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnoreShortcutWhenBusy
|
||||
{
|
||||
get => _settingsService.Settings.IgnoreShortcutWhenBusy;
|
||||
set
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { IgnoreShortcutWhenBusy = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool AllowBreakthroughShortcut
|
||||
{
|
||||
get => _settingsService.Settings.AllowBreakthroughShortcut;
|
||||
set
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { AllowBreakthroughShortcut = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool DisableAnimations
|
||||
{
|
||||
get => _settingsService.Settings.DisableAnimations;
|
||||
|
||||
@@ -1,26 +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.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
internal static class NativeMethods
|
||||
{
|
||||
[DllImport("shell32.dll")]
|
||||
public static extern int SHQueryUserNotificationState(out UserNotificationState state);
|
||||
}
|
||||
|
||||
internal enum UserNotificationState : int
|
||||
{
|
||||
QUNS_NOT_PRESENT = 1,
|
||||
QUNS_BUSY,
|
||||
QUNS_RUNNING_D3D_FULL_SCREEN,
|
||||
QUNS_PRESENTATION_MODE,
|
||||
QUNS_ACCEPTS_NOTIFICATIONS,
|
||||
QUNS_QUIET_TIME,
|
||||
QUNS_APP,
|
||||
}
|
||||
@@ -1,28 +1,115 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using System.Diagnostics;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed partial class WindowHelper
|
||||
{
|
||||
public static bool IsWindowFullscreen()
|
||||
{
|
||||
UserNotificationState state;
|
||||
/// <summary>
|
||||
/// Known applications whose visible windows can trigger a QUNS_BUSY state
|
||||
/// even when the user is not actually in a fullscreen/presentation scenario.
|
||||
/// </summary>
|
||||
internal sealed record KnownTriggerApp(string ProcessName, string WindowClassName, string DisplayName);
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
||||
if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) is null)
|
||||
internal static readonly KnownTriggerApp[] KnownTriggerWindowClasses =
|
||||
[
|
||||
new("NVIDIA overlay", "CEF-OSC-WIDGET", "NVIDIA Overlay"),
|
||||
];
|
||||
|
||||
public static QUERY_USER_NOTIFICATION_STATE? GetUserNotificationState()
|
||||
{
|
||||
if (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
|
||||
state == UserNotificationState.QUNS_BUSY ||
|
||||
state == UserNotificationState.QUNS_PRESENTATION_MODE)
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
||||
return PInvoke.SHQueryUserNotificationState(out var state).Succeeded ? state : null;
|
||||
}
|
||||
|
||||
internal static UserNotificationFlags GetUserNotificationFlags()
|
||||
{
|
||||
return GetUserNotificationFlags(GetUserNotificationState());
|
||||
}
|
||||
|
||||
internal static UserNotificationFlags GetUserNotificationFlags(QUERY_USER_NOTIFICATION_STATE? state)
|
||||
{
|
||||
return new UserNotificationFlags(
|
||||
IsRunningD3DFullScreen: state is QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN,
|
||||
IsPresentationMode: state is QUERY_USER_NOTIFICATION_STATE.QUNS_PRESENTATION_MODE,
|
||||
IsBusy: state is QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the display names of known trigger apps that currently have visible
|
||||
/// windows matching their expected window class and process name.
|
||||
/// </summary>
|
||||
public static unsafe List<string> FindVisibleTriggerApps()
|
||||
{
|
||||
var detected = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
PInvoke.EnumWindows(
|
||||
(HWND hWnd, LPARAM lParam) =>
|
||||
{
|
||||
if (!PInvoke.IsWindowVisible(hWnd))
|
||||
{
|
||||
return true; // continue
|
||||
}
|
||||
|
||||
var className = GetWindowClassName(hWnd);
|
||||
if (className is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var app in KnownTriggerWindowClasses)
|
||||
{
|
||||
if (!string.Equals(className, app.WindowClassName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Window class matches — verify the process name.
|
||||
uint pid;
|
||||
if (PInvoke.GetWindowThreadProcessId(hWnd, &pid) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var process = Process.GetProcessById((int)pid);
|
||||
if (string.Equals(process.ProcessName, app.ProcessName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
detected.Add(app.DisplayName);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Process may have exited between enumeration and lookup.
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
0);
|
||||
|
||||
return [.. detected];
|
||||
}
|
||||
|
||||
private static unsafe string? GetWindowClassName(HWND hWnd)
|
||||
{
|
||||
const int maxLength = 256;
|
||||
fixed (char* buffer = new char[maxLength])
|
||||
{
|
||||
var length = PInvoke.GetClassName(hWnd, buffer, maxLength);
|
||||
return length > 0 ? new string(buffer, 0, length) : null;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct UserNotificationFlags(bool IsRunningD3DFullScreen, bool IsPresentationMode, bool IsBusy)
|
||||
{
|
||||
public bool IsFullscreenState => IsRunningD3DFullScreen || IsPresentationMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
|
||||
private readonly List<long> _breakthroughTimestamps = [];
|
||||
|
||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||
private bool _ignoreHotKeyWhenBusy;
|
||||
private bool _allowBreakthroughShortcut;
|
||||
private bool _suppressDpiChange;
|
||||
private bool _themeServiceInitialized;
|
||||
|
||||
@@ -369,6 +373,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
|
||||
|
||||
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
|
||||
_ignoreHotKeyWhenBusy = settings.IgnoreShortcutWhenBusy;
|
||||
_allowBreakthroughShortcut = settings.AllowBreakthroughShortcut;
|
||||
|
||||
_autoGoHomeInterval = settings.AutoGoHomeInterval;
|
||||
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
||||
@@ -1208,10 +1214,25 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void HandleSummon(string commandId)
|
||||
{
|
||||
if (_ignoreHotKeyWhenFullScreen)
|
||||
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
||||
if (isRootHotkey && IsPaletteVisibleToUser())
|
||||
{
|
||||
// If we're in full screen mode, ignore the hotkey
|
||||
if (WindowHelper.IsWindowFullscreen())
|
||||
HandleSummonCore(commandId);
|
||||
return;
|
||||
}
|
||||
|
||||
var notificationFlags = WindowHelper.GetUserNotificationFlags();
|
||||
var shouldSuppress =
|
||||
(_ignoreHotKeyWhenFullScreen && notificationFlags.IsFullscreenState) ||
|
||||
(_ignoreHotKeyWhenBusy && notificationFlags.IsBusy);
|
||||
|
||||
if (shouldSuppress)
|
||||
{
|
||||
if (_allowBreakthroughShortcut && IsBreakthroughTriggered())
|
||||
{
|
||||
// Rapid-press breakthrough: let it through
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1220,12 +1241,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
HandleSummonCore(commandId);
|
||||
}
|
||||
|
||||
private void HandleSummonCore(string commandId)
|
||||
private bool IsPaletteVisibleToUser()
|
||||
{
|
||||
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
||||
|
||||
var isVisible = this.Visible;
|
||||
var isVisible = Visible;
|
||||
|
||||
unsafe
|
||||
{
|
||||
@@ -1240,6 +1258,36 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
private bool IsBreakthroughTriggered()
|
||||
{
|
||||
const int requiredPresses = 3;
|
||||
var windowTicks = 2 * Stopwatch.Frequency; // 2 seconds
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
|
||||
_breakthroughTimestamps.Add(now);
|
||||
|
||||
// Prune timestamps outside the window
|
||||
_breakthroughTimestamps.RemoveAll(t => now - t > windowTicks);
|
||||
|
||||
if (_breakthroughTimestamps.Count >= requiredPresses)
|
||||
{
|
||||
_breakthroughTimestamps.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleSummonCore(string commandId)
|
||||
{
|
||||
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
||||
|
||||
var isVisible = IsPaletteVisibleToUser();
|
||||
|
||||
// Note to future us: the wParam will have the index of the hotkey we registered.
|
||||
// We can use that in the future to differentiate the hotkeys we've pressed
|
||||
// so that we can bind hotkeys to individual commands
|
||||
|
||||
@@ -112,3 +112,7 @@ AttachThreadInput
|
||||
GetWindowPlacement
|
||||
WINDOWPLACEMENT
|
||||
WM_DPICHANGED
|
||||
|
||||
QUERY_USER_NOTIFICATION_STATE
|
||||
EnumWindows
|
||||
IsWindowVisible
|
||||
|
||||
@@ -55,7 +55,20 @@
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard" IsChecked="{x:Bind viewModel.IgnoreShortcutWhenFullscreen, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_IgnoreShortcutWhenBusy_SettingsCard" IsChecked="{x:Bind viewModel.IgnoreShortcutWhenBusy, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_AllowBreakthroughShortcut_SettingsCard" IsChecked="{x:Bind viewModel.AllowBreakthroughShortcut, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
<controls:SettingsExpander.ItemsFooter>
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind IsNotificationStateSuppressing, Mode=OneWay}"
|
||||
Message="{x:Bind NotificationStateMessage, Mode=OneWay}"
|
||||
Severity="Warning" />
|
||||
</controls:SettingsExpander.ItemsFooter>
|
||||
</controls:SettingsExpander>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
|
||||
|
||||
@@ -2,22 +2,32 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Globalization;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Win32.UI.Shell;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
public sealed partial class GeneralPage : Page
|
||||
public sealed partial class GeneralPage : Page, INotifyPropertyChanged
|
||||
{
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
private readonly SettingsViewModel? viewModel;
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly DispatcherTimer _notificationStateTimer;
|
||||
|
||||
private bool _isNotificationStateSuppressing;
|
||||
private string _notificationStateMessage = string.Empty;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public GeneralPage()
|
||||
{
|
||||
@@ -25,9 +35,41 @@ public sealed partial class GeneralPage : Page
|
||||
|
||||
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
|
||||
_settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
|
||||
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
|
||||
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
|
||||
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, _settingsService);
|
||||
|
||||
_notificationStateTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
||||
_notificationStateTimer.Tick += NotificationStateTimer_Tick;
|
||||
|
||||
Loaded += GeneralPage_Loaded;
|
||||
Unloaded += GeneralPage_Unloaded;
|
||||
}
|
||||
|
||||
public bool IsNotificationStateSuppressing
|
||||
{
|
||||
get => _isNotificationStateSuppressing;
|
||||
private set
|
||||
{
|
||||
if (_isNotificationStateSuppressing != value)
|
||||
{
|
||||
_isNotificationStateSuppressing = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNotificationStateSuppressing)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string NotificationStateMessage
|
||||
{
|
||||
get => _notificationStateMessage;
|
||||
private set
|
||||
{
|
||||
if (_notificationStateMessage != value)
|
||||
{
|
||||
_notificationStateMessage = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NotificationStateMessage)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ApplicationVersion
|
||||
@@ -39,4 +81,85 @@ public sealed partial class GeneralPage : Page
|
||||
return string.Format(CultureInfo.CurrentCulture, versionNo, version);
|
||||
}
|
||||
}
|
||||
|
||||
private void GeneralPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_settingsService.SettingsChanged += SettingsService_SettingsChanged;
|
||||
UpdateNotificationState();
|
||||
_notificationStateTimer.Start();
|
||||
}
|
||||
|
||||
private void GeneralPage_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_notificationStateTimer.Stop();
|
||||
_settingsService.SettingsChanged -= SettingsService_SettingsChanged;
|
||||
}
|
||||
|
||||
private void NotificationStateTimer_Tick(object? sender, object e)
|
||||
{
|
||||
UpdateNotificationState();
|
||||
}
|
||||
|
||||
private void SettingsService_SettingsChanged(ISettingsService sender, SettingsModel settings)
|
||||
{
|
||||
if (DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
UpdateNotificationState();
|
||||
return;
|
||||
}
|
||||
|
||||
DispatcherQueue.TryEnqueue(UpdateNotificationState);
|
||||
}
|
||||
|
||||
private void UpdateNotificationState()
|
||||
{
|
||||
var state = WindowHelper.GetUserNotificationState();
|
||||
var notificationFlags = WindowHelper.GetUserNotificationFlags(state);
|
||||
|
||||
if (IsActivationShortcutSuppressed(
|
||||
notificationFlags.IsFullscreenState,
|
||||
notificationFlags.IsBusy,
|
||||
viewModel?.IgnoreShortcutWhenFullscreen == true,
|
||||
viewModel?.IgnoreShortcutWhenBusy == true))
|
||||
{
|
||||
var stateDescription = state switch
|
||||
{
|
||||
QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN => ResourceLoaderInstance.GetString("NotificationState_D3DFullScreen"),
|
||||
QUERY_USER_NOTIFICATION_STATE.QUNS_PRESENTATION_MODE => ResourceLoaderInstance.GetString("NotificationState_PresentationMode"),
|
||||
QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY => ResourceLoaderInstance.GetString("NotificationState_Busy"),
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
var messageFormat = ResourceLoaderInstance.GetString("Settings_GeneralPage_NotificationState_InfoBar");
|
||||
var message = string.Format(CultureInfo.CurrentCulture, messageFormat, stateDescription);
|
||||
|
||||
if (state is QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY)
|
||||
{
|
||||
var triggerApps = WindowHelper.FindVisibleTriggerApps();
|
||||
if (triggerApps.Count > 0)
|
||||
{
|
||||
var triggerFormat = ResourceLoaderInstance.GetString("NotificationState_TriggerApps");
|
||||
message += " " + string.Format(CultureInfo.CurrentCulture, triggerFormat, string.Join(", ", triggerApps));
|
||||
}
|
||||
}
|
||||
|
||||
NotificationStateMessage = message;
|
||||
IsNotificationStateSuppressing = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationStateMessage = string.Empty;
|
||||
IsNotificationStateSuppressing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsActivationShortcutSuppressed(
|
||||
bool isFullscreenState,
|
||||
bool isBusyState,
|
||||
bool ignoreShortcutWhenFullscreen,
|
||||
bool ignoreShortcutWhenBusy)
|
||||
{
|
||||
return (ignoreShortcutWhenFullscreen && isFullscreenState) ||
|
||||
(ignoreShortcutWhenBusy && isBusyState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +347,33 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_IgnoreShortcutWhenBusy_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Ignore shortcut when the system heuristically detects fullscreen</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_IgnoreShortcutWhenBusy_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Windows may detect that a fullscreen application is running or Presentation Settings are applied. Some applications (such as NVIDIA overlay) can trigger this incorrectly, preventing the shortcut from working</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AllowBreakthroughShortcut_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Allow breakthrough with rapid shortcut presses</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AllowBreakthroughShortcut_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Press the activation shortcut 3 times within 2 seconds to bypass the fullscreen or busy state guard</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_NotificationState_InfoBar" xml:space="preserve">
|
||||
<value>Your activation shortcut might not work right now because {0}.</value>
|
||||
</data>
|
||||
<data name="NotificationState_D3DFullScreen" xml:space="preserve">
|
||||
<value>a full-screen app is running</value>
|
||||
</data>
|
||||
<data name="NotificationState_PresentationMode" xml:space="preserve">
|
||||
<value>presentation mode is on</value>
|
||||
</data>
|
||||
<data name="NotificationState_Busy" xml:space="preserve">
|
||||
<value>Windows thinks a full-screen app or presentation is active</value>
|
||||
</data>
|
||||
<data name="NotificationState_TriggerApps" xml:space="preserve">
|
||||
<value>Likely caused by: {0}.</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Highlight search on activate</value>
|
||||
</data>
|
||||
|
||||
Reference in New Issue
Block a user