Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.10" />
<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" />
<!-- 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.
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.260203002" />
Expand Down Expand Up @@ -153,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
54 changes: 41 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 Down Expand Up @@ -240,21 +239,25 @@ private void SendInputAndHideToolbar(InputType inputType)

private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)
{
shiftPressed = shiftPressed || WindowsFunctions.IsShiftState();

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;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

shiftPressed = shiftPressed || WindowsFunctions.IsShiftState() will treat any currently-held Shift as “navigate backwards”, including Shift that was already down to type an uppercase letter. This appears to contradict the keyboard hook’s existing behavior of only tracking Shift after the toolbar is visible (to avoid uppercase conflicts), and can cause Space navigation to unexpectedly go backwards when the user triggers Quick Accent while holding Shift for capitalization. Consider tracking the Shift state at toolbar-show time (and only applying the async-key fallback if Shift transitioned after the toolbar became visible), or moving the fallback into the hook where you can distinguish “Shift used for uppercase” vs “Shift used for navigation” reliably.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was an excellent catch. I've implemented the suggested logic in commit 59261e0. The new code tracks the initial state of the shift key when the popup is summoned, i.e. the user is typing a capital letter, and checks against this when determining the subsequent shift state in ProcessNextChar, disregarding the current hardware shift state if the initial state was true, i.e. only relying on the keyboard hook in that case. This means we still get the better async shift state handling for lowercase letters and we don't accidentally move backwards when the user is typing a capital letter.

}

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 +324,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;
}
}
1 change: 1 addition & 0 deletions src/modules/poweraccent/PowerAccent.UI/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SetWindowPos
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"
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