diff --git a/Directory.Packages.props b/Directory.Packages.props index 4a488adfdb29..778f17195c87 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -68,14 +68,14 @@ - + - + @@ -153,4 +153,4 @@ - + \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccent.Core/NativeMethods.txt b/src/modules/poweraccent/PowerAccent.Core/NativeMethods.txt index 6aeae331c34e..9de1fe83ca1f 100644 --- a/src/modules/poweraccent/PowerAccent.Core/NativeMethods.txt +++ b/src/modules/poweraccent/PowerAccent.Core/NativeMethods.txt @@ -1,6 +1,7 @@ -GetDpiForWindow GetGUIThreadInfo GetKeyState GetMonitorInfo MonitorFromWindow -SendInput \ No newline at end of file +SendInput +GetAsyncKeyState +GetDpiForMonitor \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs index 4811118adbd1..e21ee4b600c3 100644 --- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Text; using System.Unicode; -using System.Windows; using ManagedCommon; using PowerAccent.Core.Services; @@ -27,6 +26,7 @@ public partial class PowerAccent : IDisposable private string[] _characterDescriptions = Array.Empty(); private int _selectedIndex = -1; private bool _showUnicodeDescription; + private bool _initialShiftState; // Was shift held down when the toolbar was summoned? public LetterKey[] LetterKeysShowingDescription => _letterKeysShowingDescription; @@ -95,6 +95,7 @@ private void SetEvents() private void ShowToolbar(LetterKey letterKey) { + _initialShiftState = WindowsFunctions.IsShiftState(); _visible = true; _characters = GetCharacters(letterKey); @@ -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) @@ -321,22 +331,47 @@ private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed) OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]); } + /// + /// Calculates the coordinates at which a window of the specified size should be + /// displayed, based on the current display settings and user preferences. + /// + /// The calculated coordinates take into account the active display's + /// location, size, DPI, and the user's configured position preferences. + /// The size of the window for which to calculate display + /// coordinates. + /// 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. 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); } + /// + /// Gets the maximum width for the toolbar display based on the active screen + /// dimensions. + /// + /// The maximum width in logical pixels, accounting for screen padding. + /// 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; } + /// + /// Gets the user-configured position preference for the toolbar display. For example + /// . + /// + /// The preferred location for the toolbar. public Position GetToolbarPosition() { return _settingService.Position; diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs index a790d27fb593..c4f479c4d2b3 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs @@ -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; @@ -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()); } @@ -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); } @@ -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; } } diff --git a/src/modules/poweraccent/PowerAccent.UI/NativeMethods.txt b/src/modules/poweraccent/PowerAccent.UI/NativeMethods.txt new file mode 100644 index 000000000000..bff40be3a3b1 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/NativeMethods.txt @@ -0,0 +1,2 @@ +SetWindowPos +GetSystemMetrics diff --git a/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj b/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj index 30111fbdf7b0..b1ce71bd7ab7 100644 --- a/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj +++ b/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj @@ -25,6 +25,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml index 6345a89fbdc8..372b861db59e 100644 --- a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml +++ b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml @@ -14,6 +14,7 @@ ResizeMode="NoResize" ShowInTaskbar="False" SizeToContent="WidthAndHeight" + SizeChanged="Window_SizeChanged" Visibility="Collapsed" WindowBackdropType="None" WindowStyle="None" @@ -51,16 +52,19 @@ HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" Background="Transparent" + ScrollViewer.HorizontalScrollBarVisibility="Auto" + Focusable="False" IsHitTestVisible="False">