Skip to content

[Shortcut Guide] Restore long-press Windows key activation#48583

Draft
niels9001 wants to merge 5 commits into
microsoft:mainfrom
niels9001:niels9001/restore-shortcut-guide-long-press-win
Draft

[Shortcut Guide] Restore long-press Windows key activation#48583
niels9001 wants to merge 5 commits into
microsoft:mainfrom
niels9001:niels9001/restore-shortcut-guide-long-press-win

Conversation

@niels9001

Copy link
Copy Markdown
Collaborator

Summary of the Pull Request

Restores the pre-V2 long-press Windows key activation for Shortcut Guide. Users can now choose between the customized keyboard shortcut (default Win + Shift + /) or holding the Windows key for a configurable duration (default 900 ms). The two modes are mutually exclusive, matching the v0.99 behavior.

Closes #48491

PR Checklist

  • Closes: [Shortcut Guide] Restore Press and Hold shortcut #48491
  • Communication: I've discussed this with core contributors already.
  • Tests: Added unit tests for the new view-model properties (defaults, persistence, serialization round-trip).
  • Manual tests: Verified switching activation modes triggers immediate runner re-registration; long-press fires the overlay; conflict warning surfaces appropriately.
  • Localization: New strings (ShortcutGuide_PressTime.Header, .Description) added; the activation-method and warning strings already existed in Resources.resw from v0.99 and are reused as-is.

Detailed Description of the Pull Request / Additional comments

New settings (settings.json)

Key Type Default
use_legacy_press_win_key_behavior bool false
press_time int (ms) 900

When use_legacy_press_win_key_behavior is true:

  • The C++ module returns std::nullopt from GetHotkeyEx() so the runner does not register the customized shortcut.
  • The module returns true from keep_track_of_pressed_win_key() and the configured ms from milliseconds_win_key_must_be_pressed(), which causes powertoy_module.cpp::UpdateHotkeyEx to register a press-action on VK_LWIN/VK_RWIN via CentralizedKeyboardHook::AddPressedKeyAction.
  • Toggling the setting in the Settings UI fires IPC -> set_config -> UpdateHotkeyEx immediately, so no restart is needed.

Simplifications vs v0.99

v0.99 had two press-time settings (press_time and press_time_for_taskbar_icon_shortcuts). V2 shows the taskbar indicator inline whenever the selected section has a <TASKBAR1-9> marker, so the dual-stage timing is no longer meaningful. This PR ships one press_time setting.

Backwards compat

  • Existing V2 users with no extra keys -> defaults apply, no behavior change.
  • Users upgraded from v0.99 silently pick up their existing use_legacy_press_win_key_behavior / press_time values.

Files changed

File Change
ShortcutGuideProperties.cs Added UseLegacyPressWinKeyBehavior and PressTime (default 900 ms)
ShortcutGuideModuleInterface/dllmain.cpp Parses new keys; overrides keep_track_of_pressed_win_key / milliseconds_win_key_must_be_pressed; gates GetHotkeyEx
ShortcutGuideViewModel.cs New observable properties wired through NotifyPropertyChanged (which both raises change and saves+sends IPC)
ShortcutGuidePage.xaml New Activation method ComboBox; the existing shortcut card and the new press-duration NumberBox toggle visibility; warning InfoBar
Resources.resw New ShortcutGuide_PressTime.Header / .Description
Settings.UI.UnitTests/ViewModelTests/ShortcutGuide.cs 3 new tests

Validation Steps Performed

  • Settings.UI.Library, ShortcutGuideModuleInterface, PowerToys.Settings, Settings.UI.UnitTests all build clean (exit 0, empty errors logs) on arm64 / Debug.
  • Settings.UI.UnitTests -> all 10 ShortcutGuide tests pass, including the 3 new ones.
  • Manually toggled the new ComboBox; verified the shortcut card hides, the press-duration NumberBox appears, and changing the duration takes effect without restart.
  • Verified long-press triggers the overlay; verified switching back to Custom shortcut re-registers the shortcut.

Brings back the pre-V2 activation mode where users could open Shortcut
Guide by holding the Windows key (default 900ms) instead of pressing a
customized shortcut. The two modes are mutually exclusive, matching the
v0.99 behavior.

New settings:
- use_legacy_press_win_key_behavior (bool, default false)
- press_time (int ms, default 900)

The C++ module overrides keep_track_of_pressed_win_key() and
milliseconds_win_key_must_be_pressed(); when legacy mode is on it also
returns nullopt from GetHotkeyEx() so the runner doesn't register the
custom shortcut. The runner already handles dynamic re-registration via
CentralizedKeyboardHook::AddPressedKeyAction and UpdateHotkeyEx().

The Settings page exposes a 'Activation method' ComboBox; selecting
'Press and hold the Windows key' hides the shortcut control, shows a
press-duration NumberBox, and surfaces the existing warning InfoBar
about possible side-effects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@niels9001 niels9001 marked this pull request as draft June 13, 2026 16:25
Two runner-side bugs caused the long-press Win activation to misfire:

1. UpdateHotkeyEx() did not clear pre-existing pressed-key descriptors
   before re-adding them. Because the runner calls UpdateHotkeyEx() on
   every settings update -- and from several general-settings paths
   without first calling update_hotkeys() -- the LWIN/RWIN descriptors
   would accumulate. PressedKeyTimerProc iterates all descriptors that
   share a timer id, so one Win-hold ended up firing OnHotkeyEx() N
   times (N launches, then "Another instance is running" errors).
   Fix: introduce ClearModulePressedKeyActions() and call it at the top
   of the win-key branch in UpdateHotkeyEx(); also gate on is_enabled()
   to match the regular hotkey path. ClearModuleHotkeys is refactored
   to reuse the new helper and now kills any in-flight timers.

2. PressedKeyTimerProc ignored the action's return value, so even when
   the action wanted Start Menu suppression we never sent the dummy
   keystroke. The result: the user's Win-key release activated the
   Start Menu directly over the freshly-shown Shortcut Guide window.
   Fix: honor the bool return -- when true, send the same WM_KEYUP
   dummy event KeyboardHookProc uses for regular hotkeys. The Shortcut
   Guide action lambda now returns true.

A break is also added after the matching action so a single timer id
fires its action exactly once even if duplicate descriptors slip
through.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

The dummy 0xFF keystroke that prevents the Start Menu from opening after a long-press action was being sent at timer-fire time -- ~press_time ms before the actual Win release. By the time the user released Win, the Windows shell had already forgotten the injection and Start Menu would still pop up.

Match the v0.99 ShortcutGuide pattern: record a pending chord-break when the timer fires, then inject the dummy keystroke inside the WH_KEYBOARD_LL hook when the matching Win-up arrives, before CallNextHookEx propagates it to the shell.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Make the toggle path explicit in the trace log so it's obvious when an alternating press is dismissing a held-over Debug-build instance vs failing to launch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

WinUI3's dispatcher loop does not auto-exit when the last window closes (unlike WPF/WinForms). Without an explicit Application.Exit() call, Application.Start in Program.cs never returns and SG.exe lingers after its window is closed.

The runner's module then sees IsProcessActive() == true on the next OnHotkeyEx, takes the toggle/dismiss path, and TerminateProcess's the zombie instead of launching a new window. Net result: every other hotkey press appears to do nothing.

Call Microsoft.UI.Xaml.Application.Current.Exit() in the MainWindow.Closed handler so the process actually terminates after the window goes away.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

@check-spelling-bot Report

🔴 Please review

See the 📂 files view, the 📜action log, 👼 SARIF report, or 📝 job summary for details.

❌ Errors and Warnings Count
⚠️ binary-file 1
⚠️ duplicate-pattern 2
❌ forbidden-pattern 1
⚠️ large-file 1

See ❌ Event descriptions for more information.

Some files were automatically ignored 🙈

These sample patterns would exclude them:

^src/modules/ZoomIt/ZoomIt/rnnoise/rnnoise_data_little\.c$
^src/modules/ZoomIt/ZoomIt/selfie_segmentation\.onnx$

You should consider adding them to:

.github/actions/spell-check/excludes.txt

File matching is via Perl regular expressions.

To check these files, more of their words need to be in the dictionary than not. You can use patterns.txt to exclude portions, add items to the dictionary (e.g. by adding them to allow.txt), or fix typos.

To update file exclusions, you could run the following commands

... in a clone of the git@github.com:niels9001/PowerToys.git repository
on the niels9001/restore-shortcut-guide-long-press-win branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/27473829785/attempts/1' &&
git commit -m 'Update check-spelling metadata'

Forbidden patterns 🙅 (1)

In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.

These forbidden patterns matched content:

Should be a
\san (?=(?:[b-dfgjklpqtvwz]|h(?!onou?r|our|s[lv]|tml|ttp|ref)|n(?!ginx|grok|pm)|r(?!c)|s(?!s[ho]|vg))[a-z]|x(?!\b|[-\d]|ml))
If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Shortcut Guide] Restore Press and Hold shortcut

1 participant