Skip to content

[QuickAccent] Fix UI glitches, DPI-related issues, selection bugs, and add hardware shift key state fallback#46593

Open
daverayment wants to merge 7 commits into
microsoft:mainfrom
daverayment:fix-quick-accent-ui
Open

[QuickAccent] Fix UI glitches, DPI-related issues, selection bugs, and add hardware shift key state fallback#46593
daverayment wants to merge 7 commits into
microsoft:mainfrom
daverayment:fix-quick-accent-ui

Conversation

@daverayment
Copy link
Copy Markdown
Collaborator

Summary of the Pull Request

This PR fixes several issues around the popup selection window's size and position, selection-related issues which result in flashing or glitching, and includes more reliable detection of the Shift key.

PR Checklist

Detailed Description of the Pull Request / Additional comments

This PR includes fixes for the Quick Accent's selection window position, its width measurement, and letter selection-related issues. In addition, glitches such as the window flashing the selection colour and the window appearing blank should be reduced or eliminated entirely.

Popup width bug

When opening Quick Accent from a letter with many mappings, it would appear too wide for the display. Even though letters could be selected, they may be entirely off-screen:

image

This was because of this flaw in GetDisplayMaxWidth, which is used directly by the popup to set the maximum width of the characters area:

    // In Selector.xaml.cs
    private void SetWindowsSize()
    {
        this.characters.MaxWidth = _powerAccent.GetDisplayMaxWidth();
    }

...
    // In PowerAccent.cs
    public double GetDisplayMaxWidth()
    {
        return WindowsFunctions.GetActiveDisplay().Size.Width - ScreenMinPadding;
    }

GetActiveDisplay uses the GetMonitorInfo API, which exposes the working area of the display. It returns its values in raw unscaled pixel values:

    public static (Point Location, Size Size, double Dpi) GetActiveDisplay()
    {
        ...
        var res = PInvoke.MonitorFromWindow(guiInfo.hwndActive, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
        MONITORINFO monitorInfo = default;
        monitorInfo.cbSize = (uint)Marshal.SizeOf(monitorInfo);
        PInvoke.GetMonitorInfo(res, ref monitorInfo);

        ...

        return (location, monitorInfo.rcWork.Size, dpi);
    }

However, the MaxWidth property must be a pre-scaled value, i.e. in logical WPF units not physical pixels. The fix is straightforward:

    public double GetDisplayMaxWidth()
    {
        var activeDisplay = WindowsFunctions.GetActiveDisplay();
        return (activeDisplay.Size.Width / activeDisplay.Dpi) - ScreenMinPadding;
    }

Popup positioning bug

This is related to a subtle DPI issue in GetActiveDisplay():

    public static (Point Location, Size Size, double Dpi) GetActiveDisplay()
    {
        GUITHREADINFO guiInfo = default;
        guiInfo.cbSize = (uint)Marshal.SizeOf(guiInfo);
        PInvoke.GetGUIThreadInfo(0, ref guiInfo);
        var res = PInvoke.MonitorFromWindow(guiInfo.hwndActive, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);

        MONITORINFO monitorInfo = default;
        monitorInfo.cbSize = (uint)Marshal.SizeOf(monitorInfo);
        PInvoke.GetMonitorInfo(res, ref monitorInfo);

        double dpi = PInvoke.GetDpiForWindow(guiInfo.hwndActive) / 96d;
        var location = new Point(monitorInfo.rcWork.left, monitorInfo.rcWork.top);
        return (location, monitorInfo.rcWork.Size, dpi);
    }

Here, the application window's DPI is returned. Unfortunately, the window may report a value which is different from the monitor's own DPI value. This will consistently happen if the application is not Per-Monitor DPI-Aware, and the monitor is not at 100% Scale. The effects are that the Quick Accent popup can appear misaligned or even off-screen entirely. Quick Accent can still be used, but the user may not be able to see what they are selecting.

As Quick Accent is using monitor coordinates for setting its location, the solution is to use the monitor's own DPI value. The fix is to add this in place of the GetDpiForWindow line:

        uint dpiRaw = 96; // Safe default
        if (PInvoke.GetDpiForMonitor(res, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out uint dpiX, out _) == 0)
        {
            dpiRaw = dpiX;
        }

        double dpi = dpiRaw / 96d;

Selection bugs

After dismissing the Quick Accent window, the _selectedIndex state was not properly reset. The next time the window opened, it could attempt to scroll to or highlight an index that was out-of-bounds for the new character set. This could result in glitching, such as the window flashing the selection colour or the initial selection being incorrect. In this fix, I:

  1. Explicitly set _selectedIndex to -1 when the UI hides.
  2. Reset the SelectedIndex inside Selector.xaml.cs before updating the ItemsSource.

Shift key activation

In certain cases, a quick press of Shift could fail to move back through the character list. In this fix, I:

  1. Added a native fallback usign GetAsyncKeyState(VK_SHIFT).
  2. Updated ProcessNextChar() to evaluate shiftPressed || WindowsFunctions.IsShiftState().
  3. Updated the multiple ifs in ProcessNextChar to be an if/else structure, to prevent bugs when more than one trigger key is pressed.

Support added for multi-codepoint graphemes

The current code loops through each char of a mapping, calling SendInput multiple times for multi-char sequences. This will fail for multi-codepoint graphemes, i.e. where the mapping 'letter' is more than one UTF-16 codepoint. Those characters may appear as []. The amended Insert() in WindowsFunctions appends all characters before calling SendInput.

Miscelleneous

  • Added an OnDpiChanged handler for the Selector control, so changing the DPI of the screen should be picked up automatically. (It's questionable whether this is essential, as the DPI would have to change while the control was displayed, but it's worth having for robustness.)
  • Now using SetWindowPos instead of setting the Left and Top of the popup control. Also now initialising the popup offscreen to attempt to reduce flicker and the occurrence of blank window flashes.
  • Changed the Focusable property of the characters ListBox to False, to attempt to reduce flicker and the window flashing the selector colour.
  • Removed Width and added MinWidth to the letter control in Selector.xaml. This allows for wider letters or longer multi-letter mappings.
  • Changed the VirtualizingStackPanel to a regular StackPanel. We do not have mappings with enough entries for a virtual control to be necessary, and using StackPanel seemed to have a positive effect on the appearance of blank window glitches.
  • Added TextTrimming, TextWrapping and MaxHeight to the unicode description TextBlock. This helps support extremely long unicode descriptions. Again, this will enable us to support longer multi-character mappings in the future.
  • Added CsWin32 to the PowerAccent.UI project, to support the SetWindowPos call.

Validation Steps Performed

See separate doc for full test details:

https://docs.google.com/document/d/19uClcUiv7RUDRlbFhazG-Cmu46oNmrAVoJf9bHSjSJU/edit?usp=sharing

@daverayment daverayment added the Product-Quick Accent Refers to the Quick Accent PowerToy label Mar 28, 2026
@niels9001 niels9001 requested a review from Copilot March 28, 2026 11:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets the Quick Accent (PowerAccent) popup UI reliability by addressing DPI scaling/positioning issues, selection-state glitches, and improving keyboard handling for navigation.

Changes:

  • Fix DPI-related sizing/positioning by using monitor DPI and switching popup placement to Win32 SetWindowPos with raw coordinates.
  • Reduce selection flashing/glitching via selection reset, ItemsSource clearing on hide, and UI layout adjustments (min widths, trimming/wrapping).
  • Add/adjust native interop usage (CsWin32 update, new SetWindowPos projection, GetDpiForMonitor, GetAsyncKeyState) and tweak navigation logic.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs Switches positioning to SetWindowPos, adds DPI/size-change repositioning, and improves selection reset/scroll behavior.
src/modules/poweraccent/PowerAccent.UI/Selector.xaml Adjusts ListBox focus/scroll behavior, sizing (MinWidth), panel type, and description TextBlock trimming/wrapping.
src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj Adds CsWin32 package reference for UI project Win32 interop.
src/modules/poweraccent/PowerAccent.UI/NativeMethods.txt Declares SetWindowPos for CsWin32 generation in UI project.
src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs Updates text insertion to send in a single SendInput call; uses GetDpiForMonitor and GetAsyncKeyState.
src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs Adjusts selection initialization/navigation (including shift behavior) and updates coordinate/max-width calculations for DPI correctness.
src/modules/poweraccent/PowerAccent.Core/NativeMethods.txt Updates CsWin32 native method list to match new P/Invoke usage.
Directory.Packages.props Bumps Microsoft.Windows.CsWin32 package version repo-wide and applies minor formatting cleanup.

Comment thread src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs Outdated
Comment thread src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs
…ed. Ensure offscreen rendering is actually offscreen by using virtual display metrics.
@niels9001
Copy link
Copy Markdown
Collaborator

@daverayment Ah, looks like we are partying in the same app :)

#46604 replaces the WpfUi library with plain WPF and it's built-in Fluent Theming. Do you want us to review your PR first or merge that one first and sync yours then with main?

@daverayment
Copy link
Copy Markdown
Collaborator Author

@niels9001 Ooh, exciting! It looks like the controls that are changing back to pure WPF in #46604 are ones that I've also changed in this PR, so there's likely going to be a conflict whichever way around we do it :) I think that the properties I've added or updated are consistent with regular WPF.

It may be better to deal with this PR first before doing the WpfUI->WPF migration. Otherwise, the test document I wrote won't be valid.

After the migration to pure WPF, we probably need to ensure the control properties I've changed/added are still present and then run through the tests again.

@niels9001
Copy link
Copy Markdown
Collaborator

@niels9001 Ooh, exciting! It looks like the controls that are changing back to pure WPF in #46604 are ones that I've also changed in this PR, so there's likely going to be a conflict whichever way around we do it :) I think that the properties I've added or updated are consistent with regular WPF.

It may be better to deal with this PR first before doing the WpfUI->WPF migration. Otherwise, the test document I wrote won't be valid.

After the migration to pure WPF, we probably need to ensure the control properties I've changed/added are still present and then run through the tests again.

Sounds good! Let's get this one in first!

@daverayment
Copy link
Copy Markdown
Collaborator Author

@niels9001 Just in case you were waiting on me, I've made the Copilot-requested changes.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread src/modules/poweraccent/PowerAccent.UI/Selector.xaml
Comment thread src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs
Comment thread Directory.Packages.props
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.10" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.269" />
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping Microsoft.Windows.CsWin32 in Directory.Packages.props affects every project in the repo that consumes CsWin32 and increases the blast radius of this QuickAccent UI fix. If this version change is not strictly required for the new APIs used here, consider reverting the global bump (or isolating it to a separate PR) to keep this change atomic and reduce risk of unrelated build/interop diffs.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not addressed in this iteration: CsWin32 version bump (0.3.183 -> 0.3.206) has repo-wide blast radius. Escalated to maintainer for decision on whether to keep bundled with this UI fix PR or split into a separate PR.

- Change ScrollBar visibility from Auto to Hidden (non-interactive scrollbar)
- Clear characterName text when no valid selection (prevent stale display)

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

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs
Comment thread src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs
@daverayment
Copy link
Copy Markdown
Collaborator Author

@MuyuanMS Thank you for the review.

Could you please revert the change to the HorizontalScrollbarVisibility property. It is important that this is Auto so the scrollbar appears when there are too many characters to fit on screen - that was a deliberate decision and Copilot's feedback was a false positive. It is intended that the scrollbar is not keyboard accessible, as the user is using the keyboard to cycle through the characters themselves instead.

The HorizontalScrollBarVisibility=Auto is intentional - the scrollbar
provides visual feedback when there are more accent characters than fit
on screen. Keyboard-only navigation means the Copilot accessibility
warning was a false positive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MuyuanMS
Copy link
Copy Markdown
Contributor

Thanks for the feedback @daverayment! You're absolutely right — the scrollbar at Auto is intentional for visual feedback when there are more characters than fit on screen, and keyboard-only navigation means the accessibility concern doesn't apply here. Reverted in cf7adb1.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread src/modules/poweraccent/PowerAccent.UI/Selector.xaml
Comment thread Directory.Packages.props
Comment on lines 70 to 72
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.269" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="10.0.7" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment