mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [X] Closes: #981 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [X] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments This pull request implements Screencast Mode, a keyboard visualization module. The implementation of this module was made with the goal of adhering to current PowerToys development conventions. There is currently no Unit Tests to do automated testing for the module or its settings, which would need to be added in the future. For more information, there is a README that is located in src/modules/ScreencastMode <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed For validation, the module was built using the appropriate steps to get the .exe for PowerToys and that executable was run. From there, the module was manually tested from simple typing on the keyboard to changing settings around for the module. --------- Co-authored-by: Zoha Ahmed <122557699+Zoha-ahmed@users.noreply.github.com> Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Co-authored-by: Gleb Khmyznikov <gleb.khmyznikov@gmail.com> Co-authored-by: Guilherme <57814418+DevLGuilherme@users.noreply.github.com> Co-authored-by: Kai Tao <69313318+vanzue@users.noreply.github.com> Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com> Co-authored-by: leileizhang <leilzh@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jiří Polášek <me@jiripolasek.com>
1083 lines
39 KiB
C#
1083 lines
39 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using CmdPalKeyboardService;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using ManagedCommon;
|
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
|
using Microsoft.CmdPal.Core.Common.Services;
|
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
|
|
using Microsoft.CmdPal.UI.Controls;
|
|
using Microsoft.CmdPal.UI.Events;
|
|
using Microsoft.CmdPal.UI.Helpers;
|
|
using Microsoft.CmdPal.UI.Messages;
|
|
using Microsoft.CmdPal.UI.Services;
|
|
using Microsoft.CmdPal.UI.ViewModels;
|
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.PowerToys.Telemetry;
|
|
using Microsoft.UI;
|
|
using Microsoft.UI.Composition;
|
|
using Microsoft.UI.Composition.SystemBackdrops;
|
|
using Microsoft.UI.Input;
|
|
using Microsoft.UI.Windowing;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.Windows.AppLifecycle;
|
|
using Windows.ApplicationModel.Activation;
|
|
using Windows.Foundation;
|
|
using Windows.Graphics;
|
|
using Windows.System;
|
|
using Windows.UI;
|
|
using Windows.UI.WindowManagement;
|
|
using Windows.Win32;
|
|
using Windows.Win32.Foundation;
|
|
using Windows.Win32.Graphics.Dwm;
|
|
using Windows.Win32.Graphics.Gdi;
|
|
using Windows.Win32.UI.HiDpi;
|
|
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
|
using Windows.Win32.UI.WindowsAndMessaging;
|
|
using WinRT;
|
|
using WinUIEx;
|
|
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
|
|
|
namespace Microsoft.CmdPal.UI;
|
|
|
|
public sealed partial class MainWindow : WindowEx,
|
|
IRecipient<DismissMessage>,
|
|
IRecipient<ShowWindowMessage>,
|
|
IRecipient<HideWindowMessage>,
|
|
IRecipient<QuitMessage>,
|
|
IRecipient<DragStartedMessage>,
|
|
IRecipient<DragCompletedMessage>,
|
|
IDisposable
|
|
{
|
|
private const int DefaultWidth = 800;
|
|
private const int DefaultHeight = 480;
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
|
private readonly uint WM_TASKBAR_RESTART;
|
|
private readonly HWND _hwnd;
|
|
private readonly DispatcherTimer _autoGoHomeTimer;
|
|
private readonly WNDPROC? _hotkeyWndProc;
|
|
private readonly WNDPROC? _originalWndProc;
|
|
private readonly List<TopLevelHotkey> _hotkeys = [];
|
|
private readonly KeyboardListener _keyboardListener;
|
|
private readonly LocalKeyboardListener _localKeyboardListener;
|
|
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
|
|
private readonly IThemeService _themeService;
|
|
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
|
|
private bool _ignoreHotKeyWhenFullScreen = true;
|
|
private bool _themeServiceInitialized;
|
|
|
|
private DesktopAcrylicController? _acrylicController;
|
|
private SystemBackdropConfiguration? _configurationSource;
|
|
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
|
|
|
private WindowPosition _currentWindowPosition = new();
|
|
|
|
private bool _preventHideWhenDeactivated;
|
|
|
|
private MainWindowViewModel ViewModel { get; }
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
|
|
ViewModel = App.Current.Services.GetService<MainWindowViewModel>()!;
|
|
|
|
_autoGoHomeTimer = new DispatcherTimer();
|
|
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
|
|
|
|
_themeService = App.Current.Services.GetRequiredService<IThemeService>();
|
|
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
|
_windowThemeSynchronizer = new WindowThemeSynchronizer(_themeService, this);
|
|
|
|
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
|
|
|
unsafe
|
|
{
|
|
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
|
|
}
|
|
|
|
SetAcrylic();
|
|
|
|
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
|
|
|
|
_keyboardListener = new KeyboardListener();
|
|
_keyboardListener.Start();
|
|
|
|
_keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
|
|
|
|
this.SetIcon();
|
|
AppWindow.Title = RS_.GetString("AppName");
|
|
RestoreWindowPosition();
|
|
UpdateWindowPositionInMemory();
|
|
|
|
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
|
|
|
// Hide our titlebar.
|
|
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
|
|
// to hide the old caption buttons. Then, in UpdateRegionsForCustomTitleBar,
|
|
// we'll make the top drag-able again. (after our content loads)
|
|
ExtendsContentIntoTitleBar = true;
|
|
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
|
SizeChanged += WindowSizeChanged;
|
|
RootElement.Loaded += RootElementLoaded;
|
|
|
|
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
|
|
|
|
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
|
|
// member (and instead like, use a local), then the pointer we marshal
|
|
// into the WindowLongPtr will be useless after we leave this function,
|
|
// and our **WindProc will explode**.
|
|
_hotkeyWndProc = HotKeyPrc;
|
|
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
|
|
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
|
|
|
// Load our settings, and then also wire up a settings changed handler
|
|
HotReloadSettings();
|
|
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
|
|
|
|
// Make sure that we update the acrylic theme when the OS theme changes
|
|
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
|
|
|
|
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
|
|
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
|
|
{
|
|
Summon(string.Empty);
|
|
});
|
|
|
|
_localKeyboardListener = new LocalKeyboardListener();
|
|
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
|
|
_localKeyboardListener.Start();
|
|
|
|
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
|
|
HideWindow();
|
|
}
|
|
|
|
private void OnAutoGoHomeTimerOnTick(object? s, object e)
|
|
{
|
|
_autoGoHomeTimer.Stop();
|
|
|
|
// BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
|
|
// and prevent the user from opening its context menu.
|
|
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
|
|
}
|
|
|
|
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
|
{
|
|
UpdateAcrylic();
|
|
}
|
|
|
|
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
|
{
|
|
if (e.Key == VirtualKey.GoBack)
|
|
{
|
|
WeakReferenceMessenger.Default.Send(new GoBackMessage());
|
|
}
|
|
}
|
|
|
|
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
|
|
|
|
private void RootElementLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
// Now that our content has loaded, we can update our draggable regions
|
|
UpdateRegionsForCustomTitleBar();
|
|
|
|
// Add dev ribbon if enabled
|
|
if (!BuildInfo.IsCiBuild)
|
|
{
|
|
RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) });
|
|
}
|
|
}
|
|
|
|
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
|
|
|
|
private void PositionCentered()
|
|
{
|
|
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
|
|
PositionCentered(displayArea);
|
|
}
|
|
|
|
private void RestoreWindowPosition()
|
|
{
|
|
var settings = App.Current.Services.GetService<SettingsModel>();
|
|
if (settings?.LastWindowPosition is not WindowPosition savedPosition)
|
|
{
|
|
PositionCentered();
|
|
return;
|
|
}
|
|
|
|
if (savedPosition.Width <= 0 || savedPosition.Height <= 0)
|
|
{
|
|
PositionCentered();
|
|
return;
|
|
}
|
|
|
|
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
|
|
AppWindow.MoveAndResize(newRect);
|
|
}
|
|
|
|
private void PositionCentered(DisplayArea displayArea)
|
|
{
|
|
if (displayArea is not null)
|
|
{
|
|
var centeredPosition = AppWindow.Position;
|
|
centeredPosition.X = (displayArea.WorkArea.Width - AppWindow.Size.Width) / 2;
|
|
centeredPosition.Y = (displayArea.WorkArea.Height - AppWindow.Size.Height) / 2;
|
|
|
|
centeredPosition.X += displayArea.WorkArea.X;
|
|
centeredPosition.Y += displayArea.WorkArea.Y;
|
|
AppWindow.Move(centeredPosition);
|
|
}
|
|
}
|
|
|
|
private void UpdateWindowPositionInMemory()
|
|
{
|
|
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
|
|
_currentWindowPosition = new WindowPosition
|
|
{
|
|
X = AppWindow.Position.X,
|
|
Y = AppWindow.Position.Y,
|
|
Width = AppWindow.Size.Width,
|
|
Height = AppWindow.Size.Height,
|
|
Dpi = (int)this.GetDpiForWindow(),
|
|
ScreenWidth = displayArea.WorkArea.Width,
|
|
ScreenHeight = displayArea.WorkArea.Height,
|
|
};
|
|
}
|
|
|
|
private void HotReloadSettings()
|
|
{
|
|
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
|
|
|
SetupHotkey(settings);
|
|
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
|
|
|
|
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
|
|
|
|
_autoGoHomeInterval = settings.AutoGoHomeInterval;
|
|
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
|
}
|
|
|
|
private void SetAcrylic()
|
|
{
|
|
if (DesktopAcrylicController.IsSupported())
|
|
{
|
|
// Hooking up the policy object.
|
|
_configurationSource = new SystemBackdropConfiguration
|
|
{
|
|
// Initial configuration state.
|
|
IsInputActive = true,
|
|
};
|
|
UpdateAcrylic();
|
|
}
|
|
}
|
|
|
|
private void UpdateAcrylic()
|
|
{
|
|
try
|
|
{
|
|
if (_acrylicController != null)
|
|
{
|
|
_acrylicController.RemoveAllSystemBackdropTargets();
|
|
_acrylicController.Dispose();
|
|
}
|
|
|
|
var backdrop = _themeService.Current.BackdropParameters;
|
|
_acrylicController = new DesktopAcrylicController
|
|
{
|
|
TintColor = backdrop.TintColor,
|
|
TintOpacity = backdrop.TintOpacity,
|
|
FallbackColor = backdrop.FallbackColor,
|
|
LuminosityOpacity = backdrop.LuminosityOpacity,
|
|
};
|
|
|
|
// Enable the system backdrop.
|
|
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
|
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
|
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Failed to update backdrop", ex);
|
|
}
|
|
}
|
|
|
|
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
|
{
|
|
StopAutoGoHome();
|
|
|
|
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
|
|
|
|
// Remember, IsIconic == "minimized", which is entirely different state
|
|
// from "show/hide"
|
|
// If we're currently minimized, restore us first, before we reveal
|
|
// our window. Otherwise, we'd just be showing a minimized window -
|
|
// which would remain not visible to the user.
|
|
if (PInvoke.IsIconic(hwnd))
|
|
{
|
|
// Make sure our HWND is cloaked before any possible window manipulations
|
|
Cloak();
|
|
|
|
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
|
}
|
|
|
|
if (target == MonitorBehavior.ToLast)
|
|
{
|
|
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
|
AppWindow.MoveAndResize(newRect);
|
|
}
|
|
else
|
|
{
|
|
var display = GetScreen(hwnd, target);
|
|
PositionCentered(display);
|
|
}
|
|
|
|
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
|
|
// because that would make it hard to debug the app
|
|
if (Debugger.IsAttached)
|
|
{
|
|
_hiddenOwnerBehavior.ShowInTaskbar(this, true);
|
|
}
|
|
|
|
// Just to be sure, SHOW our hwnd.
|
|
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
|
|
|
|
// Once we're done, uncloak to avoid all animations
|
|
Uncloak();
|
|
|
|
PInvoke.SetForegroundWindow(hwnd);
|
|
PInvoke.SetActiveWindow(hwnd);
|
|
|
|
// Push our window to the top of the Z-order and make it the topmost, so that it appears above all other windows.
|
|
// We want to remove the topmost status when we hide the window (because we cloak it instead of hiding it).
|
|
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures that the window rectangle is visible on-screen.
|
|
/// </summary>
|
|
/// <param name="windowRect">The window rectangle in physical pixels.</param>
|
|
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
|
|
/// <param name="originalDpi">The window's original DPI.</param>
|
|
/// <returns>
|
|
/// A window rectangle in physical pixels, moved to the nearest display and resized
|
|
/// if the DPI has changed.
|
|
/// </returns>
|
|
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
|
|
{
|
|
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
|
|
if (displayArea is null)
|
|
{
|
|
return windowRect;
|
|
}
|
|
|
|
var workArea = displayArea.WorkArea;
|
|
if (workArea.Width <= 0 || workArea.Height <= 0)
|
|
{
|
|
// Fallback, nothing reasonable to do
|
|
return windowRect;
|
|
}
|
|
|
|
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
|
|
if (originalDpi <= 0)
|
|
{
|
|
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
|
|
}
|
|
|
|
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
|
|
if (hasInvalidSize)
|
|
{
|
|
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
|
|
}
|
|
|
|
// If we have a DPI change, scale the window rectangle accordingly
|
|
if (effectiveDpi != originalDpi)
|
|
{
|
|
var scalingFactor = effectiveDpi / (double)originalDpi;
|
|
windowRect = new RectInt32(
|
|
(int)Math.Round(windowRect.X * scalingFactor),
|
|
(int)Math.Round(windowRect.Y * scalingFactor),
|
|
(int)Math.Round(windowRect.Width * scalingFactor),
|
|
(int)Math.Round(windowRect.Height * scalingFactor));
|
|
}
|
|
|
|
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
|
|
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
|
|
|
|
// Ensure at least some minimum visible area (e.g., 100 pixels)
|
|
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
|
|
const int minimumVisibleSize = 100;
|
|
var isOffscreen =
|
|
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
|
|
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
|
|
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
|
|
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
|
|
|
|
// if the work area size has changed, re-center the window
|
|
var workAreaSizeChanged =
|
|
originalScreen.Width != workArea.Width ||
|
|
originalScreen.Height != workArea.Height;
|
|
|
|
int targetX;
|
|
int targetY;
|
|
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
|
|
if (recenter)
|
|
{
|
|
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
|
|
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
|
|
}
|
|
else
|
|
{
|
|
targetX = windowRect.X;
|
|
targetY = windowRect.Y;
|
|
}
|
|
|
|
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
|
|
}
|
|
|
|
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
|
|
{
|
|
var effectiveDpi = 96;
|
|
|
|
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
|
|
if (!hMonitor.IsNull)
|
|
{
|
|
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
|
|
if (hr == 0)
|
|
{
|
|
effectiveDpi = (int)dpiX;
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
|
|
}
|
|
}
|
|
|
|
if (effectiveDpi <= 0)
|
|
{
|
|
effectiveDpi = 96;
|
|
}
|
|
|
|
return effectiveDpi;
|
|
}
|
|
|
|
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
|
|
{
|
|
// Leaving a note here, in case we ever need it:
|
|
// https://github.com/microsoft/microsoft-ui-xaml/issues/6454
|
|
// If we need to ever FindAll, we'll need to iterate manually
|
|
var displayAreas = Microsoft.UI.Windowing.DisplayArea.FindAll();
|
|
switch (target)
|
|
{
|
|
case MonitorBehavior.InPlace:
|
|
if (PInvoke.GetWindowRect(currentHwnd, out var bounds))
|
|
{
|
|
RectInt32 converted = new(bounds.X, bounds.Y, bounds.Width, bounds.Height);
|
|
return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest);
|
|
}
|
|
|
|
break;
|
|
|
|
case MonitorBehavior.ToFocusedWindow:
|
|
var foregroundWindowHandle = PInvoke.GetForegroundWindow();
|
|
if (foregroundWindowHandle != IntPtr.Zero)
|
|
{
|
|
if (PInvoke.GetWindowRect(foregroundWindowHandle, out var fgBounds))
|
|
{
|
|
RectInt32 converted = new(fgBounds.X, fgBounds.Y, fgBounds.Width, fgBounds.Height);
|
|
return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case MonitorBehavior.ToPrimary:
|
|
return DisplayArea.Primary;
|
|
|
|
case MonitorBehavior.ToMouse:
|
|
default:
|
|
if (PInvoke.GetCursorPos(out var cursorPos))
|
|
{
|
|
return DisplayArea.GetFromPoint(new PointInt32(cursorPos.X, cursorPos.Y), DisplayAreaFallback.Nearest);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return DisplayArea.Primary;
|
|
}
|
|
|
|
public void Receive(ShowWindowMessage message)
|
|
{
|
|
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
|
|
|
ShowHwnd(message.Hwnd, settings.SummonOn);
|
|
}
|
|
|
|
public void Receive(HideWindowMessage message)
|
|
{
|
|
// This might come in off the UI thread. Make sure to hop back.
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
HideWindow();
|
|
});
|
|
}
|
|
|
|
public void Receive(QuitMessage message) =>
|
|
|
|
// This might come in on a background thread
|
|
DispatcherQueue.TryEnqueue(() => Close());
|
|
|
|
public void Receive(DismissMessage message)
|
|
{
|
|
if (message.ForceGoHome)
|
|
{
|
|
WeakReferenceMessenger.Default.Send(new GoHomeMessage(false, false));
|
|
}
|
|
|
|
// This might come in off the UI thread. Make sure to hop back.
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
HideWindow();
|
|
});
|
|
}
|
|
|
|
private void HideWindow()
|
|
{
|
|
// Cloak our HWND to avoid all animations.
|
|
var cloaked = Cloak();
|
|
|
|
// Then hide our HWND, to make sure that the OS gives the FG / focus back to another app
|
|
// (there's no way for us to guess what the right hwnd might be, only the OS can do it right)
|
|
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
|
|
|
if (cloaked)
|
|
{
|
|
// TRICKY: show our HWND again. This will trick XAML into painting our
|
|
// HWND again, so that we avoid the "flicker" caused by a WinUI3 app
|
|
// window being first shown
|
|
// SW_SHOWNA will prevent us for trying to fight the focus back
|
|
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_SHOWNA);
|
|
|
|
// Intentionally leave the window cloaked. So our window is "visible",
|
|
// but also cloaked, so you can't see it.
|
|
|
|
// If the window was not cloaked, then leave it hidden.
|
|
// Sure, it's not ideal, but at least it's not visible.
|
|
}
|
|
|
|
// Start auto-go-home timer
|
|
RestartAutoGoHome();
|
|
}
|
|
|
|
private void StopAutoGoHome()
|
|
{
|
|
_autoGoHomeTimer.Stop();
|
|
}
|
|
|
|
private void RestartAutoGoHome()
|
|
{
|
|
if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_autoGoHomeTimer.Stop();
|
|
_autoGoHomeTimer.Start();
|
|
}
|
|
|
|
private bool Cloak()
|
|
{
|
|
bool wasCloaked;
|
|
unsafe
|
|
{
|
|
BOOL value = true;
|
|
var hr = PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
|
|
if (hr.Failed)
|
|
{
|
|
Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
|
|
}
|
|
|
|
wasCloaked = hr.Succeeded;
|
|
}
|
|
|
|
if (wasCloaked)
|
|
{
|
|
// Because we're only cloaking the window, bury it at the bottom in case something can
|
|
// see it - e.g. some accessibility helper (note: this also removes the top-most status).
|
|
PInvoke.SetWindowPos(_hwnd, HWND.HWND_BOTTOM, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
|
}
|
|
|
|
return wasCloaked;
|
|
}
|
|
|
|
private void Uncloak()
|
|
{
|
|
unsafe
|
|
{
|
|
BOOL value = false;
|
|
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
|
|
}
|
|
}
|
|
|
|
internal void MainWindow_Closed(object sender, WindowEventArgs args)
|
|
{
|
|
var serviceProvider = App.Current.Services;
|
|
UpdateWindowPositionInMemory();
|
|
|
|
var settings = serviceProvider.GetService<SettingsModel>();
|
|
if (settings is not null)
|
|
{
|
|
settings.LastWindowPosition = new WindowPosition
|
|
{
|
|
X = _currentWindowPosition.X,
|
|
Y = _currentWindowPosition.Y,
|
|
Width = _currentWindowPosition.Width,
|
|
Height = _currentWindowPosition.Height,
|
|
Dpi = _currentWindowPosition.Dpi,
|
|
ScreenWidth = _currentWindowPosition.ScreenWidth,
|
|
ScreenHeight = _currentWindowPosition.ScreenHeight,
|
|
};
|
|
|
|
SettingsModel.SaveSettings(settings);
|
|
}
|
|
|
|
var extensionService = serviceProvider.GetService<IExtensionService>()!;
|
|
extensionService.SignalStopExtensionsAsync();
|
|
|
|
App.Current.Services.GetService<TrayIconService>()!.Destroy();
|
|
|
|
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
|
|
// Workaround by turning it off before shutdown.
|
|
App.Current.DebugSettings.FailFastOnErrors = false;
|
|
_localKeyboardListener.Dispose();
|
|
DisposeAcrylic();
|
|
|
|
_keyboardListener.Stop();
|
|
Environment.Exit(0);
|
|
}
|
|
|
|
private void DisposeAcrylic()
|
|
{
|
|
if (_acrylicController is not null)
|
|
{
|
|
_acrylicController.Dispose();
|
|
_acrylicController = null!;
|
|
_configurationSource = null!;
|
|
}
|
|
}
|
|
|
|
// Updates our window s.t. the top of the window is draggable.
|
|
private void UpdateRegionsForCustomTitleBar()
|
|
{
|
|
// Specify the interactive regions of the title bar.
|
|
var scaleAdjustment = RootElement.XamlRoot.RasterizationScale;
|
|
|
|
// Get the rectangle around our XAML content. We're going to mark this
|
|
// rectangle as "Passthrough", so that the normal window operations
|
|
// (resizing, dragging) don't apply in this space.
|
|
var transform = RootElement.TransformToVisual(null);
|
|
|
|
// Reserve 16px of space at the top for dragging.
|
|
var topHeight = 16;
|
|
var bounds = transform.TransformBounds(new Rect(
|
|
0,
|
|
topHeight,
|
|
RootElement.ActualWidth,
|
|
RootElement.ActualHeight));
|
|
var contentRect = GetRect(bounds, scaleAdjustment);
|
|
var rectArray = new RectInt32[] { contentRect };
|
|
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id);
|
|
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray);
|
|
|
|
// Add a drag-able region on top
|
|
var w = RootElement.ActualWidth;
|
|
_ = RootElement.ActualHeight;
|
|
var dragSides = new RectInt32[]
|
|
{
|
|
GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall
|
|
};
|
|
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Caption, dragSides);
|
|
}
|
|
|
|
private static RectInt32 GetRect(Rect bounds, double scale)
|
|
{
|
|
return new RectInt32(
|
|
_X: (int)Math.Round(bounds.X * scale),
|
|
_Y: (int)Math.Round(bounds.Y * scale),
|
|
_Width: (int)Math.Round(bounds.Width * scale),
|
|
_Height: (int)Math.Round(bounds.Height * scale));
|
|
}
|
|
|
|
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
|
|
{
|
|
if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
|
|
{
|
|
try
|
|
{
|
|
_themeService.Initialize();
|
|
_themeServiceInitialized = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Failed to initialize ThemeService", ex);
|
|
}
|
|
}
|
|
|
|
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
|
{
|
|
// Save the current window position before hiding the window
|
|
UpdateWindowPositionInMemory();
|
|
|
|
// If there's a debugger attached...
|
|
if (System.Diagnostics.Debugger.IsAttached)
|
|
{
|
|
// ... then don't hide the window when it loses focus.
|
|
return;
|
|
}
|
|
|
|
// Are we disabled? If we are, then we don't want to dismiss on focus lost.
|
|
// This can happen if an extension wanted to show a modal dialog on top of our
|
|
// window i.e. in the case of an MSAL auth window.
|
|
if (PInvoke.IsWindowEnabled(_hwnd) == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We're doing something that requires us to lose focus, but we don't want to hide the window
|
|
if (_preventHideWhenDeactivated)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// This will DWM cloak our window:
|
|
HideWindow();
|
|
|
|
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
|
|
}
|
|
|
|
if (_configurationSource is not null)
|
|
{
|
|
_configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
|
|
}
|
|
}
|
|
|
|
public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs)
|
|
{
|
|
// LOAD BEARING
|
|
// Any reading and processing of the activation arguments must be done
|
|
// synchronously in this method, before it returns. The sending instance
|
|
// remains blocked until this returns; afterward it may quit, causing
|
|
// the activation arguments to be lost.
|
|
if (activatedEventArgs is null)
|
|
{
|
|
Summon(string.Empty);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (activatedEventArgs.Kind == ExtendedActivationKind.StartupTask)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (activatedEventArgs.Kind == ExtendedActivationKind.Protocol)
|
|
{
|
|
if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs)
|
|
{
|
|
if (protocolArgs.Uri.ToString() is string uri)
|
|
{
|
|
// was the URI "x-cmdpal://background" ?
|
|
if (uri.StartsWith("x-cmdpal://background", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// we're running, we don't want to activate our window. bail
|
|
return;
|
|
}
|
|
else if (uri.StartsWith("x-cmdpal://settings", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
|
return;
|
|
}
|
|
else if (uri.StartsWith("x-cmdpal://reload", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var settings = App.Current.Services.GetService<SettingsModel>();
|
|
if (settings?.AllowExternalReload == true)
|
|
{
|
|
Logger.LogInfo("External Reload triggered");
|
|
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInfo("External Reload is disabled");
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (COMException ex)
|
|
{
|
|
// https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-return-values
|
|
const int RPC_S_SERVER_UNAVAILABLE = -2147023174;
|
|
const int RPC_S_CALL_FAILED = 2147023170;
|
|
|
|
// Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
|
|
// if the args are not valid or not passed correctly.
|
|
if (ex.HResult is RPC_S_SERVER_UNAVAILABLE or RPC_S_CALL_FAILED)
|
|
{
|
|
Logger.LogWarning(
|
|
$"COM exception (HRESULT {ex.HResult}) when accessing activation arguments. " +
|
|
$"This might be due to the calling application not passing them correctly or exiting before we could read them. " +
|
|
$"The application will continue running and fall back to showing the Command Palette window.");
|
|
}
|
|
else
|
|
{
|
|
Logger.LogError(
|
|
$"COM exception (HRESULT {ex.HResult}) when activating the application. " +
|
|
$"The application will continue running and fall back to showing the Command Palette window.",
|
|
ex);
|
|
}
|
|
}
|
|
|
|
Summon(string.Empty);
|
|
}
|
|
|
|
public void Summon(string commandId) =>
|
|
|
|
// The actual showing and hiding of the window will be done by the
|
|
// ShellPage. This is because we don't want to show the window if the
|
|
// user bound a hotkey to just an invokable command, which we can't
|
|
// know till the message is being handled.
|
|
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(commandId, _hwnd));
|
|
|
|
private void UnregisterHotkeys()
|
|
{
|
|
_keyboardListener.ClearHotkeys();
|
|
|
|
while (_hotkeys.Count > 0)
|
|
{
|
|
PInvoke.UnregisterHotKey(_hwnd, _hotkeys.Count - 1);
|
|
_hotkeys.RemoveAt(_hotkeys.Count - 1);
|
|
}
|
|
}
|
|
|
|
private void SetupHotkey(SettingsModel settings)
|
|
{
|
|
UnregisterHotkeys();
|
|
|
|
var globalHotkey = settings.Hotkey;
|
|
if (globalHotkey is not null)
|
|
{
|
|
if (settings.UseLowLevelGlobalHotkey)
|
|
{
|
|
_keyboardListener.SetHotkeyAction(globalHotkey.Win, globalHotkey.Ctrl, globalHotkey.Shift, globalHotkey.Alt, (byte)globalHotkey.Code, string.Empty);
|
|
|
|
_hotkeys.Add(new(globalHotkey, string.Empty));
|
|
}
|
|
else
|
|
{
|
|
var vk = globalHotkey.Code;
|
|
var modifiers =
|
|
(globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
|
|
(globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
|
|
(globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
|
|
(globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
|
|
;
|
|
|
|
var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
|
|
if (success)
|
|
{
|
|
_hotkeys.Add(new(globalHotkey, string.Empty));
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var commandHotkey in settings.CommandHotkeys)
|
|
{
|
|
var key = commandHotkey.Hotkey;
|
|
|
|
if (key is not null)
|
|
{
|
|
if (settings.UseLowLevelGlobalHotkey)
|
|
{
|
|
_keyboardListener.SetHotkeyAction(key.Win, key.Ctrl, key.Shift, key.Alt, (byte)key.Code, commandHotkey.CommandId);
|
|
|
|
_hotkeys.Add(new(globalHotkey, string.Empty));
|
|
}
|
|
else
|
|
{
|
|
var vk = key.Code;
|
|
var modifiers =
|
|
(key.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
|
|
(key.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
|
|
(key.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
|
|
(key.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
|
|
;
|
|
|
|
var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
|
|
if (success)
|
|
{
|
|
_hotkeys.Add(commandHotkey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleSummon(string commandId)
|
|
{
|
|
if (_ignoreHotKeyWhenFullScreen)
|
|
{
|
|
// If we're in full screen mode, ignore the hotkey
|
|
if (WindowHelper.IsWindowFullscreen())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
HandleSummonCore(commandId);
|
|
}
|
|
|
|
private void HandleSummonCore(string commandId)
|
|
{
|
|
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
|
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
|
|
|
var isVisible = this.Visible;
|
|
|
|
unsafe
|
|
{
|
|
// We need to check if our window is cloaked or not. A cloaked window is still
|
|
// technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
|
|
// (these are all separate states)
|
|
long attr = 0;
|
|
PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
|
|
if (attr == 1 /* DWM_CLOAKED_APP */)
|
|
{
|
|
isVisible = false;
|
|
}
|
|
}
|
|
|
|
// Note to future us: the wParam will have the index of the hotkey we registered.
|
|
// We can use that in the future to differentiate the hotkeys we've pressed
|
|
// so that we can bind hotkeys to individual commands
|
|
if (!isVisible || !isRootHotkey)
|
|
{
|
|
Summon(commandId);
|
|
}
|
|
else if (isRootHotkey)
|
|
{
|
|
// If there's a debugger attached...
|
|
if (System.Diagnostics.Debugger.IsAttached)
|
|
{
|
|
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
|
|
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
|
|
Cloak();
|
|
this.Hide();
|
|
|
|
return;
|
|
}
|
|
|
|
HideWindow();
|
|
}
|
|
}
|
|
|
|
private LRESULT HotKeyPrc(
|
|
HWND hwnd,
|
|
uint uMsg,
|
|
WPARAM wParam,
|
|
LPARAM lParam)
|
|
{
|
|
switch (uMsg)
|
|
{
|
|
// Prevent the window from maximizing when double-clicking the title bar area
|
|
case PInvoke.WM_NCLBUTTONDBLCLK:
|
|
return (LRESULT)IntPtr.Zero;
|
|
case PInvoke.WM_HOTKEY:
|
|
{
|
|
var hotkeyIndex = (int)wParam.Value;
|
|
if (hotkeyIndex < _hotkeys.Count)
|
|
{
|
|
var hotkey = _hotkeys[hotkeyIndex];
|
|
HandleSummon(hotkey.CommandId);
|
|
}
|
|
|
|
return (LRESULT)IntPtr.Zero;
|
|
}
|
|
|
|
default:
|
|
if (uMsg == WM_TASKBAR_RESTART)
|
|
{
|
|
HotReloadSettings();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_localKeyboardListener.Dispose();
|
|
_windowThemeSynchronizer.Dispose();
|
|
DisposeAcrylic();
|
|
}
|
|
|
|
public void Receive(DragStartedMessage message)
|
|
{
|
|
_preventHideWhenDeactivated = true;
|
|
}
|
|
|
|
public void Receive(DragCompletedMessage message)
|
|
{
|
|
_preventHideWhenDeactivated = false;
|
|
Task.Delay(200).ContinueWith(_ =>
|
|
{
|
|
DispatcherQueue.TryEnqueue(StealForeground);
|
|
});
|
|
}
|
|
|
|
private unsafe void StealForeground()
|
|
{
|
|
var foregroundWindow = PInvoke.GetForegroundWindow();
|
|
if (foregroundWindow == _hwnd)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// This is bad, evil, and I'll have to forgo today's dinner dessert to punish myself
|
|
// for writing this. But there's no way to make this work without it.
|
|
// If the window is not reactivated, the UX breaks down: a deactivated window has to
|
|
// be activated and then deactivated again to hide.
|
|
var currentThreadId = PInvoke.GetCurrentThreadId();
|
|
var foregroundThreadId = PInvoke.GetWindowThreadProcessId(foregroundWindow, null);
|
|
if (foregroundThreadId != currentThreadId)
|
|
{
|
|
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, true);
|
|
PInvoke.SetForegroundWindow(_hwnd);
|
|
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, false);
|
|
}
|
|
else
|
|
{
|
|
PInvoke.SetForegroundWindow(_hwnd);
|
|
}
|
|
}
|
|
}
|