Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="10.0.7" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<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.

<PackageVersion Include="Microsoft.Windows.Compatibility" Version="10.0.7" />
Comment on lines 70 to 72
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />

<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
<!--
TODO: in Common.Dotnet.CsWinRT.props, on upgrade, verify RemoveCsWinRTPackageAnalyzer is no longer needed.
Expand Down Expand Up @@ -152,4 +153,4 @@
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
</ItemGroup>
</Project>
</Project>
5 changes: 3 additions & 2 deletions src/modules/poweraccent/PowerAccent.Core/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
GetDpiForWindow
GetGUIThreadInfo
GetKeyState
GetMonitorInfo
MonitorFromWindow
SendInput
SendInput
GetAsyncKeyState
GetDpiForMonitor
61 changes: 48 additions & 13 deletions src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Globalization;
using System.Text;
using System.Unicode;
using System.Windows;

using ManagedCommon;
using PowerAccent.Core.Services;
Expand All @@ -27,6 +26,7 @@ public partial class PowerAccent : IDisposable
private string[] _characterDescriptions = Array.Empty<string>();
private int _selectedIndex = -1;
private bool _showUnicodeDescription;
private bool _initialShiftState; // Was shift held down when the toolbar was summoned?

public LetterKey[] LetterKeysShowingDescription => _letterKeysShowingDescription;

Expand Down Expand Up @@ -95,6 +95,7 @@ private void SetEvents()

private void ShowToolbar(LetterKey letterKey)
{
_initialShiftState = WindowsFunctions.IsShiftState();
_visible = true;

_characters = GetCharacters(letterKey);
Expand Down Expand Up @@ -240,21 +241,30 @@ private void SendInputAndHideToolbar(InputType inputType)

private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)
{
// Use an async hardware check as a fallback in case the keyboard hook misses a
// quick Shift press. If the popup was opened while holding Shift (e.g., typing a
// capital letter), ignore the hardware check so we don't accidentally trigger a
// backwards navigation.
bool isHardwareShiftPressed = WindowsFunctions.IsShiftState() && !_initialShiftState;
shiftPressed = shiftPressed || isHardwareShiftPressed;

if (_visible && _selectedIndex == -1)
{
if (triggerKey == TriggerKey.Left)
if (triggerKey == TriggerKey.Space)
{
_selectedIndex = (_characters.Length / 2) - 1;
_selectedIndex = shiftPressed ? (_characters.Length - 1) : 0;
}

if (triggerKey == TriggerKey.Right)
else if (_settingService.StartSelectionFromTheLeft)
{
_selectedIndex = _characters.Length / 2;
_selectedIndex = 0;
}

if (triggerKey == TriggerKey.Space || _settingService.StartSelectionFromTheLeft)
else if (triggerKey == TriggerKey.Left)
{
_selectedIndex = 0;
_selectedIndex = (_characters.Length / 2) - 1;
}
else if (triggerKey == TriggerKey.Right)
{
_selectedIndex = _characters.Length / 2;
}

if (_selectedIndex < 0)
Expand Down Expand Up @@ -321,22 +331,47 @@ private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)
OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]);
}

/// <summary>
/// Calculates the coordinates at which a window of the specified size should be
/// displayed, based on the current display settings and user preferences.
/// </summary>
/// <remarks>The calculated coordinates take into account the active display's
/// location, size, DPI, and the user's configured position preferences.</remarks>
/// <param name="window">The size of the window for which to calculate display
/// coordinates.</param>
/// <returns>A point representing the top-left coordinates where the window should be
/// positioned on the active display, in physical/raw coordinates suitable for Win32
/// APIs like SetWindowPos.</returns>
public Point GetDisplayCoordinates(Size window)
{
(Point Location, Size Size, double Dpi) activeDisplay = WindowsFunctions.GetActiveDisplay();
Rect screen = new(activeDisplay.Location, activeDisplay.Size);
Position position = _settingService.Position;

/* Debug.WriteLine("Dpi: " + activeDisplay.Dpi); */

return Calculation.GetRawCoordinatesFromPosition(position, screen, window, activeDisplay.Dpi) / activeDisplay.Dpi;
return Calculation.GetRawCoordinatesFromPosition(position, screen, window, activeDisplay.Dpi);
}

/// <summary>
/// Gets the maximum width for the toolbar display based on the active screen
/// dimensions.
/// </summary>
/// <returns>The maximum width in logical pixels, accounting for screen padding.
/// </returns>
public double GetDisplayMaxWidth()
{
return WindowsFunctions.GetActiveDisplay().Size.Width - ScreenMinPadding;
// Note: activeDisplay.Size.Width is in raw physical pixels.
// We divide by DPI to convert to WPF logical pixels (Device-Independent Pixels),
// because ScreenMinPadding is a logical pixel value and WPF MaxWidth expects
// logical pixels.
var activeDisplay = WindowsFunctions.GetActiveDisplay();
return (activeDisplay.Size.Width / activeDisplay.Dpi) - ScreenMinPadding;
}

/// <summary>
/// Gets the user-configured position preference for the toolbar display. For example
/// <see cref="Position.TopLeft"/>.
/// </summary>
/// <returns>The preferred location for the toolbar.</returns>
public Position GetToolbarPosition()
{
return _settingService.Position;
Expand Down
31 changes: 19 additions & 12 deletions src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using Windows.Win32;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.HiDpi;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;

Expand Down Expand Up @@ -51,36 +52,36 @@ public static void Insert(string s, bool back = false)
Thread.Sleep(1); // Some apps, like Terminal, need a little wait to process the sent backspace or they'll ignore it.
}

foreach (char c in s)
if (s.Length > 0)
{
// Letter
var inputsInsert = new INPUT[]
var inputsInsert = new INPUT[s.Length * 2];
for (int i = 0; i < s.Length; i++)
{
new INPUT
inputsInsert[i * 2] = new INPUT
{
type = INPUT_TYPE.INPUT_KEYBOARD,
Anonymous = new INPUT._Anonymous_e__Union
{
ki = new KEYBDINPUT
{
wScan = c,
wScan = s[i],
dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_UNICODE,
},
},
},
new INPUT
};
inputsInsert[(i * 2) + 1] = new INPUT
{
type = INPUT_TYPE.INPUT_KEYBOARD,
Anonymous = new INPUT._Anonymous_e__Union
{
ki = new KEYBDINPUT
{
wScan = c,
wScan = s[i],
dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_UNICODE | KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP,
},
},
},
};
};
}

_ = PInvoke.SendInput(inputsInsert, Marshal.SizeOf<INPUT>());
}
Expand All @@ -98,7 +99,13 @@ public static (Point Location, Size Size, double Dpi) GetActiveDisplay()
monitorInfo.cbSize = (uint)Marshal.SizeOf(monitorInfo);
PInvoke.GetMonitorInfo(res, ref monitorInfo);

double dpi = PInvoke.GetDpiForWindow(guiInfo.hwndActive) / 96d;
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;
var location = new Point(monitorInfo.rcWork.left, monitorInfo.rcWork.top);
return (location, monitorInfo.rcWork.Size, dpi);
}
Expand All @@ -111,7 +118,7 @@ public static bool IsCapsLockState()

public static bool IsShiftState()
{
var shift = PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT);
var shift = PInvoke.GetAsyncKeyState((int)VIRTUAL_KEY.VK_SHIFT);
return shift < 0;
}
}
2 changes: 2 additions & 0 deletions src/modules/poweraccent/PowerAccent.UI/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SetWindowPos
GetSystemMetrics
4 changes: 4 additions & 0 deletions src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="WPF-UI" />
</ItemGroup>

Expand Down
14 changes: 11 additions & 3 deletions src/modules/poweraccent/PowerAccent.UI/Selector.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ResizeMode="NoResize"
ShowInTaskbar="False"
SizeToContent="WidthAndHeight"
SizeChanged="Window_SizeChanged"
Visibility="Collapsed"
WindowBackdropType="None"
WindowStyle="None"
Expand Down Expand Up @@ -51,16 +52,19 @@
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
Comment thread
MuyuanMS marked this conversation as resolved.
Focusable="False"
IsHitTestVisible="False">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Focusable" Value="False" />
<Setter Property="ContentTemplate" Value="{StaticResource DefaultKeyTemplate}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid
Width="48"
Height="48"
MinWidth="48"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Expand Down Expand Up @@ -95,23 +99,27 @@
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsItemsHost="False" Orientation="Horizontal" />
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>

<Grid
Grid.Row="1"
MinWidth="600"
MaxWidth="{Binding ActualWidth, ElementName=characters}"
Background="{DynamicResource LayerOnAcrylicFillColorDefaultBrush}"
Visibility="{Binding CharacterNameVisibility, UpdateSourceTrigger=PropertyChanged}">
<TextBlock
x:Name="characterName"
MaxHeight="36"
Margin="8"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="(U+0000) A COOL LETTER NAME COMES HERE"
TextAlignment="Center" />
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap" />
<Rectangle
Height="1"
HorizontalAlignment="Stretch"
Expand Down
Loading
Loading