mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 03:37:59 +01:00
466 lines
14 KiB
C#
466 lines
14 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;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Reflection;
|
||
|
|
using System.Runtime.InteropServices;
|
||
|
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
|
||
|
|
using KeystrokeOverlayUI.Controls;
|
||
|
|
using KeystrokeOverlayUI.Helpers;
|
||
|
|
using KeystrokeOverlayUI.Models;
|
||
|
|
using KeystrokeOverlayUI.Services;
|
||
|
|
|
||
|
|
using Microsoft.UI;
|
||
|
|
using Microsoft.UI.Composition.SystemBackdrops;
|
||
|
|
using Microsoft.UI.Input;
|
||
|
|
using Microsoft.UI.Windowing;
|
||
|
|
using Microsoft.UI.Xaml;
|
||
|
|
using Microsoft.UI.Xaml.Controls;
|
||
|
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||
|
|
using Microsoft.UI.Xaml.Data;
|
||
|
|
using Microsoft.UI.Xaml.Input;
|
||
|
|
using Microsoft.UI.Xaml.Media;
|
||
|
|
using Microsoft.UI.Xaml.Navigation;
|
||
|
|
using Windows.Foundation;
|
||
|
|
using Windows.Foundation.Collections;
|
||
|
|
using WinRT.Interop;
|
||
|
|
|
||
|
|
namespace KeystrokeOverlayUI
|
||
|
|
{
|
||
|
|
// Main overlay window.
|
||
|
|
public sealed partial class MainWindow : Window, IDisposable
|
||
|
|
{
|
||
|
|
public MainViewModel ViewModel { get; set; } = new();
|
||
|
|
|
||
|
|
// readonly constants
|
||
|
|
private readonly InputCursor _dragCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeAll);
|
||
|
|
private readonly DispatcherTimer _zOrderEnforcer = new();
|
||
|
|
|
||
|
|
// core components
|
||
|
|
private readonly KeystrokeListener _keystrokeListener = new();
|
||
|
|
private readonly OverlaySettings _overlaySettings = new();
|
||
|
|
private CancellationTokenSource _startupCancellationSource;
|
||
|
|
private bool _disposed;
|
||
|
|
|
||
|
|
// draggable overlay
|
||
|
|
private bool _isDragging;
|
||
|
|
private NativeWindowHelper.POINT _lastCursorPos;
|
||
|
|
|
||
|
|
public MainWindow()
|
||
|
|
{
|
||
|
|
InitializeComponent();
|
||
|
|
SystemBackdrop = new MicaBackdrop() { Kind = MicaKind.BaseAlt };
|
||
|
|
ForceWindowOnTop();
|
||
|
|
|
||
|
|
Activated += (s, e) => ApplyOverlayStyles();
|
||
|
|
_zOrderEnforcer.Interval = TimeSpan.FromMilliseconds(500);
|
||
|
|
_zOrderEnforcer.Tick += (s, e) => ForceWindowOnTop();
|
||
|
|
|
||
|
|
RootGrid.DataContext = ViewModel;
|
||
|
|
|
||
|
|
_overlaySettings.SettingsUpdated += (props) =>
|
||
|
|
{
|
||
|
|
DispatcherQueue.TryEnqueue(() => ViewModel.ApplySettings(props));
|
||
|
|
};
|
||
|
|
|
||
|
|
_overlaySettings.Initialize();
|
||
|
|
|
||
|
|
_keystrokeListener.OnBatchReceived += OnKeyReceived;
|
||
|
|
_keystrokeListener.Start();
|
||
|
|
|
||
|
|
ConfigureOverlayWindow();
|
||
|
|
RunStartupSequence(isDraggable: ViewModel.IsDraggable);
|
||
|
|
|
||
|
|
ViewModel.HotkeyActionTriggered += OnHotkeyActionTriggered;
|
||
|
|
|
||
|
|
ViewModel.PropertyChanged += (s, e) =>
|
||
|
|
{
|
||
|
|
if (e.PropertyName == nameof(MainViewModel.IsDraggable))
|
||
|
|
{
|
||
|
|
RunStartupSequence(isDraggable: ViewModel.IsDraggable);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ApplyOverlayStyles()
|
||
|
|
{
|
||
|
|
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||
|
|
NativeWindowHelper.SetOverlayWindowStyles(hWnd);
|
||
|
|
NativeWindowHelper.SetRoundedCorners(hWnd);
|
||
|
|
|
||
|
|
ForceWindowOnTop();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ConfigureOverlayWindow()
|
||
|
|
{
|
||
|
|
var appWindow = GetAppWindow();
|
||
|
|
|
||
|
|
if (appWindow != null)
|
||
|
|
{
|
||
|
|
var presenter = appWindow.Presenter as OverlappedPresenter
|
||
|
|
?? OverlappedPresenter.Create();
|
||
|
|
|
||
|
|
appWindow.SetPresenter(presenter);
|
||
|
|
|
||
|
|
presenter.IsAlwaysOnTop = true;
|
||
|
|
presenter.IsResizable = false;
|
||
|
|
presenter.IsMinimizable = false;
|
||
|
|
presenter.IsMaximizable = false;
|
||
|
|
presenter.SetBorderAndTitleBar(false, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async void RunStartupSequence(bool isDraggable)
|
||
|
|
{
|
||
|
|
_startupCancellationSource?.Cancel();
|
||
|
|
_startupCancellationSource?.Dispose();
|
||
|
|
_startupCancellationSource = new CancellationTokenSource();
|
||
|
|
|
||
|
|
var token = _startupCancellationSource.Token;
|
||
|
|
|
||
|
|
ForceWindowOnTop();
|
||
|
|
if (!_zOrderEnforcer.IsEnabled)
|
||
|
|
{
|
||
|
|
_zOrderEnforcer.Start();
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
if (isDraggable)
|
||
|
|
{
|
||
|
|
// Loop with cancellation check
|
||
|
|
for (int index = 10; index > 0; index--)
|
||
|
|
{
|
||
|
|
ViewModel.RegisterKey($"Drag to Position ({index})", durationMs: 1000, textSize: 30);
|
||
|
|
await Task.Delay(1000, token);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// cleanup
|
||
|
|
ViewModel.ClearKeys();
|
||
|
|
await Task.Delay(500, token);
|
||
|
|
}
|
||
|
|
catch (OperationCanceledException)
|
||
|
|
{
|
||
|
|
ViewModel.ClearKeys();
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
// cleanup
|
||
|
|
if (_startupCancellationSource != null)
|
||
|
|
{
|
||
|
|
_startupCancellationSource.Dispose();
|
||
|
|
_startupCancellationSource = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnKeyReceived(KeystrokeEvent kEvent)
|
||
|
|
{
|
||
|
|
if (_startupCancellationSource != null && !_startupCancellationSource.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
_startupCancellationSource.Cancel();
|
||
|
|
}
|
||
|
|
|
||
|
|
DispatcherQueue.TryEnqueue(() =>
|
||
|
|
{
|
||
|
|
ViewModel.HandleKeystrokeEvent(kEvent);
|
||
|
|
|
||
|
|
if (!_zOrderEnforcer.IsEnabled)
|
||
|
|
{
|
||
|
|
_zOrderEnforcer.Start();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ----------------------
|
||
|
|
// Hotkey Methods
|
||
|
|
// ----------------------
|
||
|
|
private void OnHotkeyActionTriggered(object sender, HotkeyAction action)
|
||
|
|
{
|
||
|
|
switch (action)
|
||
|
|
{
|
||
|
|
case HotkeyAction.Monitor:
|
||
|
|
MoveToNextMonitor();
|
||
|
|
break;
|
||
|
|
case HotkeyAction.DisplayMode:
|
||
|
|
case HotkeyAction.Activation:
|
||
|
|
HandleActivation();
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
ForceWindowOnTop();
|
||
|
|
|
||
|
|
// resize to show labels
|
||
|
|
RootGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||
|
|
var desiredSize = RootGrid.DesiredSize;
|
||
|
|
ResizeAppWindow(desiredSize.Width + 5, desiredSize.Height + 15);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void HandleActivation()
|
||
|
|
{
|
||
|
|
ShowAppWindow();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void MoveToNextMonitor()
|
||
|
|
{
|
||
|
|
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||
|
|
WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||
|
|
AppWindow appWindow = AppWindow.GetFromWindowId(wndId);
|
||
|
|
|
||
|
|
if (appWindow == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var displayAreas = DisplayArea.FindAll();
|
||
|
|
if (displayAreas.Count <= 1)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find current display index
|
||
|
|
DisplayArea currentDisplay = DisplayArea.GetFromWindowId(wndId, DisplayAreaFallback.Primary);
|
||
|
|
|
||
|
|
int currentIndex = -1;
|
||
|
|
for (int i = 0; i < displayAreas.Count; i++)
|
||
|
|
{
|
||
|
|
if (displayAreas[i].DisplayId.Value == currentDisplay.DisplayId.Value)
|
||
|
|
{
|
||
|
|
currentIndex = i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate Next Index
|
||
|
|
int nextIndex = (currentIndex + 1) % displayAreas.Count;
|
||
|
|
DisplayArea nextDisplay = displayAreas[nextIndex];
|
||
|
|
|
||
|
|
// move to Top-Left of new monitor
|
||
|
|
int newX = nextDisplay.WorkArea.X + 15;
|
||
|
|
int newY = nextDisplay.WorkArea.Y + 12;
|
||
|
|
|
||
|
|
appWindow.Move(new Windows.Graphics.PointInt32(newX, newY));
|
||
|
|
ViewModel.ShowLabel(HotkeyAction.Monitor, $"Monitor {nextIndex + 1}");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ----------------------
|
||
|
|
// Draggable Overlay
|
||
|
|
// ----------------------
|
||
|
|
private void SetRootGridCursor(InputCursor cursor)
|
||
|
|
{
|
||
|
|
// Use Reflection to access the protected "ProtectedCursor" property on the Border (RootGrid)
|
||
|
|
typeof(UIElement)
|
||
|
|
.GetProperty("ProtectedCursor", BindingFlags.NonPublic | BindingFlags.Instance)
|
||
|
|
?.SetValue(RootGrid, cursor);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RootGrid_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||
|
|
{
|
||
|
|
var properties = e.GetCurrentPoint(RootGrid).Properties;
|
||
|
|
|
||
|
|
if (ViewModel.IsDraggable && properties.IsLeftButtonPressed)
|
||
|
|
{
|
||
|
|
_isDragging = true;
|
||
|
|
RootGrid.CapturePointer(e.Pointer);
|
||
|
|
_lastCursorPos = NativeWindowHelper.GetCursorPosition();
|
||
|
|
|
||
|
|
SetRootGridCursor(_dragCursor);
|
||
|
|
e.Handled = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RootGrid_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||
|
|
{
|
||
|
|
if (_isDragging)
|
||
|
|
{
|
||
|
|
var currentPos = NativeWindowHelper.GetCursorPosition();
|
||
|
|
|
||
|
|
int deltaX = currentPos.X - _lastCursorPos.X;
|
||
|
|
int deltaY = currentPos.Y - _lastCursorPos.Y;
|
||
|
|
|
||
|
|
var appWindow = GetAppWindow();
|
||
|
|
if (appWindow != null)
|
||
|
|
{
|
||
|
|
var newPos = new Windows.Graphics.PointInt32(
|
||
|
|
appWindow.Position.X + deltaX,
|
||
|
|
appWindow.Position.Y + deltaY );
|
||
|
|
|
||
|
|
appWindow.Move(newPos);
|
||
|
|
}
|
||
|
|
|
||
|
|
_lastCursorPos = currentPos;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RootGrid_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||
|
|
{
|
||
|
|
if (_isDragging)
|
||
|
|
{
|
||
|
|
_isDragging = false;
|
||
|
|
RootGrid.ReleasePointerCapture(e.Pointer);
|
||
|
|
UpdateCursorState();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RootGrid_PointerEntered(object sender, PointerRoutedEventArgs e)
|
||
|
|
{
|
||
|
|
UpdateCursorState();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RootGrid_PointerExited(object sender, PointerRoutedEventArgs e)
|
||
|
|
{
|
||
|
|
SetRootGridCursor(null);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpdateCursorState()
|
||
|
|
{
|
||
|
|
if (ViewModel.IsDraggable)
|
||
|
|
{
|
||
|
|
SetRootGridCursor(_dragCursor);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SetRootGridCursor(null);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ----------------------
|
||
|
|
// WinUI Event Handlers
|
||
|
|
// ----------------------
|
||
|
|
private void RootGrid_Loaded(object sender, RoutedEventArgs e)
|
||
|
|
{
|
||
|
|
RootGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||
|
|
var desiredSize = RootGrid.DesiredSize;
|
||
|
|
ResizeAppWindow(desiredSize.Width + 5, desiredSize.Height + 15);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void StackPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
||
|
|
{
|
||
|
|
if (sender is not StackPanel stackPanel)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
stackPanel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||
|
|
var desiredSize = stackPanel.DesiredSize;
|
||
|
|
|
||
|
|
if (desiredSize.Width == 0 ||
|
||
|
|
desiredSize.Height == 0 ||
|
||
|
|
ViewModel.PressedKeys.Count == 0)
|
||
|
|
{
|
||
|
|
if (!ViewModel.IsVisibleHotkey)
|
||
|
|
{
|
||
|
|
HideAppWindow();
|
||
|
|
_zOrderEnforcer.Stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
ShowAppWindow();
|
||
|
|
|
||
|
|
double totalWidth =
|
||
|
|
desiredSize.Width + RootGrid.Padding.Left + RootGrid.Padding.Right + 5;
|
||
|
|
|
||
|
|
double totalHeight =
|
||
|
|
desiredSize.Height + RootGrid.Padding.Top + RootGrid.Padding.Bottom + 15;
|
||
|
|
|
||
|
|
if (ViewModel.IsVisibleHotkey)
|
||
|
|
{
|
||
|
|
totalHeight = totalHeight + 30;
|
||
|
|
}
|
||
|
|
|
||
|
|
ResizeAppWindow(totalWidth, totalHeight);
|
||
|
|
ForceWindowOnTop();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ----------------------
|
||
|
|
// Window Helper Methods
|
||
|
|
// ----------------------
|
||
|
|
private AppWindow GetAppWindow()
|
||
|
|
{
|
||
|
|
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||
|
|
WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||
|
|
return AppWindow.GetFromWindowId(wndId);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void HideAppWindow()
|
||
|
|
{
|
||
|
|
var appWindow = GetAppWindow();
|
||
|
|
appWindow?.Hide();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ShowAppWindow()
|
||
|
|
{
|
||
|
|
var appWindow = GetAppWindow();
|
||
|
|
appWindow?.Show();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ResizeAppWindow(double widthDIPs, double heightDIPs)
|
||
|
|
{
|
||
|
|
var appWindow = GetAppWindow();
|
||
|
|
|
||
|
|
if (appWindow != null)
|
||
|
|
{
|
||
|
|
double scale = RootGrid.XamlRoot.RasterizationScale;
|
||
|
|
|
||
|
|
int windowWidth = (int)Math.Ceiling(widthDIPs * scale);
|
||
|
|
int windowHeight = (int)Math.Ceiling(heightDIPs * scale);
|
||
|
|
|
||
|
|
if (appWindow.Size.Width != windowWidth ||
|
||
|
|
appWindow.Size.Height != windowHeight)
|
||
|
|
{
|
||
|
|
appWindow.Resize(new Windows.Graphics.SizeInt32(windowWidth, windowHeight));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ForceWindowOnTop()
|
||
|
|
{
|
||
|
|
if (ViewModel.PressedKeys.Count == 0)
|
||
|
|
{
|
||
|
|
_zOrderEnforcer.Stop();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||
|
|
NativeWindowHelper.EnforceTopMost(hWnd);
|
||
|
|
}
|
||
|
|
|
||
|
|
// -------------------
|
||
|
|
// Other Methods
|
||
|
|
// -------------------
|
||
|
|
public void Dispose()
|
||
|
|
{
|
||
|
|
Dispose(true);
|
||
|
|
GC.SuppressFinalize(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Dispose(bool disposing)
|
||
|
|
{
|
||
|
|
if (!_disposed)
|
||
|
|
{
|
||
|
|
if (disposing)
|
||
|
|
{
|
||
|
|
_keystrokeListener?.Dispose();
|
||
|
|
_overlaySettings?.Dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
_disposed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
~MainWindow()
|
||
|
|
{
|
||
|
|
Dispose(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|