This commit is contained in:
@@ -21,12 +21,6 @@ internal static class PopupWindowNative
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
|
||||
int X, int Y, int cx, int cy, uint uFlags);
|
||||
@@ -61,12 +55,6 @@ internal static class PopupWindowNative
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
private const int GWL_EXSTYLE = -20;
|
||||
// The window does not take focus away from the active application
|
||||
private const int WS_EX_NOACTIVATE = 0x08000000;
|
||||
// And does not show up in Alt+Tab
|
||||
private const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
|
||||
private const uint SWP_NOSIZE = 0x0001;
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
@@ -80,16 +68,6 @@ internal static class PopupWindowNative
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The popup shows up on top of other applications, so it must neither
|
||||
/// activate itself nor steal input focus from the active window.
|
||||
/// </summary>
|
||||
internal static void MakePassive(IntPtr hWnd)
|
||||
{
|
||||
int exStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
|
||||
SetWindowLong(hWnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the window to a screen point without changing its size or z-order.
|
||||
/// The coordinates are physical pixels: monitors have different scaling, while
|
||||
@@ -101,21 +79,6 @@ internal static class PopupWindowNative
|
||||
SetWindowPos(hWnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the window position and size in physical pixels.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The size is set this way rather than through Width/Height: on the first show the
|
||||
/// window is still subject to the system minimum window size (SM_CXMIN×SM_CYMIN)
|
||||
/// and the popup comes out noticeably larger than its text. By the time this is
|
||||
/// called the window is already shown and has become a popup window, which that
|
||||
/// restriction does not apply to.
|
||||
/// </remarks>
|
||||
internal static void SetBounds(IntPtr hWnd, int x, int y, int width, int height)
|
||||
{
|
||||
SetWindowPos(hWnd, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
/// <summary>The scale of the monitor the point is on (1.0 at 96 DPI).</summary>
|
||||
internal static double GetScaleAt(Point point) =>
|
||||
GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST));
|
||||
|
||||
@@ -11,16 +11,6 @@ public sealed class KeyboardLayoutOptions
|
||||
{
|
||||
/// <summary>How often to check the layout of the active window.</summary>
|
||||
public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(150);
|
||||
|
||||
/// <summary>
|
||||
/// How long to wait for a layout that has just changed to stop changing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Short on purpose: it is added to the delay before the popup shows, and it only
|
||||
/// applies while a switch is in flight. See the settling in
|
||||
/// <see cref="KeyboardLayoutService"/>.
|
||||
/// </remarks>
|
||||
public TimeSpan SettleInterval { get; init; } = TimeSpan.FromMilliseconds(40);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,18 +26,6 @@ public sealed class KeyboardLayoutOptions
|
||||
///
|
||||
/// The timer ticks on the message loop of whatever thread starts it, the same as a
|
||||
/// dispatcher timer did: the agent has a plain Win32 loop and no dispatcher to offer.
|
||||
///
|
||||
/// A switch is not reported the moment it is first seen but once the value has stopped
|
||||
/// moving. A layout change is rarely a single step: the language switcher of Windows
|
||||
/// takes the focus while it is up, and applications with a rendering engine of their
|
||||
/// own change the layout of a helper thread before that of the input window. Polling
|
||||
/// catches those in-between values, and reporting them meant the popup appearing with
|
||||
/// one layout and turning into another in front of the user.
|
||||
///
|
||||
/// The price is that the popup comes up one short tick later. While a switch is in
|
||||
/// flight the timer runs at <see cref="KeyboardLayoutOptions.SettleInterval"/> rather
|
||||
/// than at the polling interval, so that tick is a few tens of milliseconds and not
|
||||
/// another whole poll.
|
||||
/// </remarks>
|
||||
public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
{
|
||||
@@ -55,11 +33,9 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
private readonly Func<IntPtr> _getForegroundWindow;
|
||||
private readonly Func<int> _getActiveLocaleId;
|
||||
private readonly Action _requestNextLayout;
|
||||
private readonly KeyboardLayoutOptions _options;
|
||||
|
||||
private int _lastLocaleId = -1;
|
||||
private IntPtr _lastForegroundWindow;
|
||||
private int _settlingLocaleId = -1;
|
||||
|
||||
public KeyboardLayoutService(KeyboardLayoutOptions options)
|
||||
: this(
|
||||
@@ -83,7 +59,6 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
_getForegroundWindow = getForegroundWindow;
|
||||
_getActiveLocaleId = getActiveLocaleId;
|
||||
_requestNextLayout = requestNextLayout;
|
||||
_options = options;
|
||||
|
||||
_pollTimer = new MessageTimer { Interval = options.PollInterval };
|
||||
_pollTimer.Tick += OnTick;
|
||||
@@ -97,8 +72,6 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
{
|
||||
_lastForegroundWindow = _getForegroundWindow();
|
||||
_lastLocaleId = _getActiveLocaleId();
|
||||
_settlingLocaleId = -1;
|
||||
_pollTimer.Interval = _options.PollInterval;
|
||||
_pollTimer.Start();
|
||||
}
|
||||
|
||||
@@ -125,28 +98,16 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
bool appSwitched = foreground != _lastForegroundWindow;
|
||||
_lastForegroundWindow = foreground;
|
||||
|
||||
int localeId = _getActiveLocaleId();
|
||||
if (localeId == _lastLocaleId)
|
||||
{
|
||||
_lastForegroundWindow = foreground;
|
||||
Settle(inFlight: false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (localeId != _settlingLocaleId)
|
||||
{
|
||||
_settlingLocaleId = localeId;
|
||||
Settle(inFlight: true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool appSwitched = foreground != _lastForegroundWindow;
|
||||
|
||||
_lastLocaleId = localeId;
|
||||
_lastForegroundWindow = foreground;
|
||||
Settle(inFlight: false);
|
||||
|
||||
// Moving to another application with a layout of its own is not the same as
|
||||
// the user switching the layout, and the subscribers are free to react to
|
||||
@@ -157,23 +118,4 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
|
||||
LayoutChanged?.Invoke(this, new LayoutChangedEventArgs(KeyboardLayout.FromLocaleId(localeId), reason));
|
||||
}
|
||||
|
||||
// While a switch is in flight the next look comes sooner: that wait is added to the
|
||||
// delay before the popup, and a whole polling interval there would be felt
|
||||
private void Settle(bool inFlight)
|
||||
{
|
||||
if (!inFlight)
|
||||
{
|
||||
_settlingLocaleId = -1;
|
||||
}
|
||||
|
||||
TimeSpan wanted = inFlight ? _options.SettleInterval : _options.PollInterval;
|
||||
if (_pollTimer.Interval == wanted || !_pollTimer.IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pollTimer.Interval = wanted;
|
||||
_pollTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,38 +38,10 @@ public static class MessageLoop
|
||||
/// <summary>Asks the loop on this thread to finish.</summary>
|
||||
public static void Quit(int exitCode = 0) => PostQuitMessage(exitCode);
|
||||
|
||||
/// <summary>
|
||||
/// Runs everything already waiting in the queue and returns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For the tests and for the rare place that has to let a posted message through
|
||||
/// without giving up control for good.
|
||||
/// </remarks>
|
||||
public static void DrainQueue()
|
||||
{
|
||||
while (PeekMessage(out Message message, IntPtr.Zero, 0, 0, PM_REMOVE))
|
||||
{
|
||||
if (message.message == WM_QUIT)
|
||||
{
|
||||
PostQuitMessage((int)message.wParam);
|
||||
return;
|
||||
}
|
||||
|
||||
TranslateMessage(ref message);
|
||||
DispatchMessage(ref message);
|
||||
}
|
||||
}
|
||||
|
||||
private const uint WM_QUIT = 0x0012;
|
||||
private const uint PM_REMOVE = 0x0001;
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetMessageW")]
|
||||
private static extern int GetMessage(out Message lpMsg, IntPtr hWnd, uint filterMin, uint filterMax);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "PeekMessageW")]
|
||||
private static extern bool PeekMessage(
|
||||
out Message lpMsg, IntPtr hWnd, uint filterMin, uint filterMax, uint removeMsg);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool TranslateMessage(ref Message lpMsg);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user