diff --git a/.gitignore b/.gitignore index a293100e615..9a1728ebbd7 100644 --- a/.gitignore +++ b/.gitignore @@ -304,4 +304,5 @@ Output-Performance.txt # vscode .vscode -.history \ No newline at end of file +.history +/.copilot diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 635a433540f..da8b5cb19d1 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -303,13 +303,15 @@ public static unsafe bool IsForegroundWindowFullscreen() #region Pixel to DIP + private const double DefaultDpi = 96d; + /// - /// Transforms pixels to Device Independent Pixels used by WPF + /// Transforms pixels to Device Independent Pixels used by WPF. /// - /// current window, required to get presentation source - /// horizontal position in pixels - /// vertical position in pixels - /// point containing device independent pixels + /// Current window, required to get presentation source. + /// Horizontal position in pixels. + /// Vertical position in pixels. + /// Point containing device independent pixels. public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY) { Matrix matrix; @@ -324,7 +326,7 @@ public static Point TransformPixelsToDIP(Visual visual, double unitX, double uni matrix = src.CompositionTarget.TransformFromDevice; } - return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); + return new Point(matrix.M11 * unitX, matrix.M22 * unitY); } #endregion diff --git a/Flow.Launcher.Plugin/SharedModels/MonitorInfo.cs b/Flow.Launcher.Plugin/SharedModels/MonitorInfo.cs index 6808c9c9104..c5373232e5a 100644 --- a/Flow.Launcher.Plugin/SharedModels/MonitorInfo.cs +++ b/Flow.Launcher.Plugin/SharedModels/MonitorInfo.cs @@ -148,7 +148,7 @@ internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect) public string Name { get; } /// - /// Gets the display monitor rectangle, expressed in virtual-screen coordinates. + /// Gets the display monitor rectangle, expressed in virtual-screen coordinates and physical pixels. /// /// /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. @@ -156,13 +156,32 @@ internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect) public Rect Bounds { get; } /// - /// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates. + /// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates and physical pixels. /// /// /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. /// public Rect WorkingArea { get; } + /// + /// Transforms physical pixel coordinates on this monitor to WPF device-independent pixels. + /// + public Point TransformPixelsToDIP(double unitX, double unitY) + { + var (scaleX, scaleY) = GetDipScale(); + return new Point(unitX / scaleX, unitY / scaleY); + } + + /// + /// Transforms a physical pixel rectangle on this monitor to WPF device-independent pixels. + /// + public Rect TransformPixelsToDIP(Rect rect) + { + var topLeft = TransformPixelsToDIP(rect.Left, rect.Top); + var bottomRight = TransformPixelsToDIP(rect.Right, rect.Bottom); + return new Rect(topLeft, bottomRight); + } + /// /// Gets if the monitor is the primary display monitor. /// @@ -171,6 +190,24 @@ internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect) /// public override string ToString() => $"{Name} {Bounds.Width}x{Bounds.Height}"; + private (double ScaleX, double ScaleY) GetDipScale() + { + if (GetDpiForMonitor(_monitor, MonitorDpiType.EffectiveDpi, out var dpiX, out var dpiY) == 0 && dpiX != 0 && dpiY != 0) + { + return (dpiX / 96, dpiY / 96); + } + + return (1d, 1d); + } + + [DllImport("Shcore.dll")] + private static extern int GetDpiForMonitor(HMONITOR hmonitor, MonitorDpiType dpiType, out uint dpiX, out uint dpiY); + + private enum MonitorDpiType + { + EffectiveDpi = 0, + } + private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi) { fixed (MONITORINFOEXW* lpmiLocal = &lpmi) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 60517db9736..e09f9fbb309 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -395,8 +395,7 @@ private void OnLocationChanged(object sender, EventArgs e) if (IsLoaded) { - _settings.WindowLeft = Left; - _settings.WindowTop = Top; + SaveWindowPositionAndDisplayMetrics(); } } @@ -407,8 +406,7 @@ private async void OnDeactivated(object sender, EventArgs e) return; } - _settings.WindowLeft = Left; - _settings.WindowTop = Top; + SaveWindowPositionAndDisplayMetrics(); _viewModel.ClockPanelOpacity = 0.0; _viewModel.SearchIconOpacity = 0.0; @@ -537,7 +535,7 @@ private void OnMouseDown(object sender, MouseButtonEventArgs e) // Current monitor information var screen = MonitorInfo.GetNearestDisplayMonitor(new WindowInteropHelper(this).Handle); var workingArea = screen.WorkingArea; - var screenLeftTop = Win32Helper.TransformPixelsToDIP(this, workingArea.X, workingArea.Y); + var screenLeftTop = screen.TransformPixelsToDIP(workingArea.X, workingArea.Y); // Switch to Normal state WindowState = WindowState.Normal; @@ -897,10 +895,8 @@ void InitializePositionInner() { var previousScreenWidth = _settings.PreviousScreenWidth; var previousScreenHeight = _settings.PreviousScreenHeight; - GetDpi(out var previousDpiX, out var previousDpiY); - - _settings.PreviousScreenWidth = SystemParameters.VirtualScreenWidth; - _settings.PreviousScreenHeight = SystemParameters.VirtualScreenHeight; + var previousDpiX = _settings.PreviousDpiX; + var previousDpiY = _settings.PreviousDpiY; GetDpi(out var currentDpiX, out var currentDpiY); if (previousScreenWidth != 0 && previousScreenHeight != 0 && @@ -910,11 +906,13 @@ void InitializePositionInner() previousDpiX != currentDpiX || previousDpiY != currentDpiY)) { AdjustPositionForResolutionChange(); - return; } - - Left = _settings.WindowLeft; - Top = _settings.WindowTop; + else + { + Left = _settings.WindowLeft; + Top = _settings.WindowTop; + } + SaveCurrentDisplayMetrics(currentDpiX, currentDpiY); } else { @@ -938,12 +936,11 @@ void InitializePositionInner() Top = VerticalTop(screen); break; case SearchWindowAligns.Custom: - var customLeft = Win32Helper.TransformPixelsToDIP(this, - screen.WorkingArea.X + _settings.CustomWindowLeft, 0); - var customTop = Win32Helper.TransformPixelsToDIP(this, 0, + var customPosition = screen.TransformPixelsToDIP( + screen.WorkingArea.X + _settings.CustomWindowLeft, screen.WorkingArea.Y + _settings.CustomWindowTop); - Left = customLeft.X; - Top = customTop.Y; + Left = customPosition.X; + Top = customPosition.Y; break; } } @@ -954,19 +951,15 @@ private void AdjustPositionForResolutionChange() { var screenWidth = SystemParameters.VirtualScreenWidth; var screenHeight = SystemParameters.VirtualScreenHeight; - GetDpi(out var currentDpiX, out var currentDpiY); var previousLeft = _settings.WindowLeft; var previousTop = _settings.WindowTop; - GetDpi(out var previousDpiX, out var previousDpiY); var widthRatio = screenWidth / _settings.PreviousScreenWidth; var heightRatio = screenHeight / _settings.PreviousScreenHeight; - var dpiXRatio = currentDpiX / previousDpiX; - var dpiYRatio = currentDpiY / previousDpiY; - var newLeft = previousLeft * widthRatio * dpiXRatio; - var newTop = previousTop * heightRatio * dpiYRatio; + var newLeft = previousLeft * widthRatio; + var newTop = previousTop * heightRatio; var screenLeft = SystemParameters.VirtualScreenLeft; var screenTop = SystemParameters.VirtualScreenTop; @@ -994,6 +987,22 @@ private void GetDpi(out double dpiX, out double dpiY) } } + private void SaveWindowPositionAndDisplayMetrics() + { + _settings.WindowLeft = Left; + _settings.WindowTop = Top; + GetDpi(out var dpiX, out var dpiY); + SaveCurrentDisplayMetrics(dpiX, dpiY); + } + + private void SaveCurrentDisplayMetrics(double dpiX, double dpiY) + { + _settings.PreviousScreenWidth = SystemParameters.VirtualScreenWidth; + _settings.PreviousScreenHeight = SystemParameters.VirtualScreenHeight; + _settings.PreviousDpiX = dpiX; + _settings.PreviousDpiY = dpiY; + } + private MonitorInfo SelectedScreen() { MonitorInfo screen; @@ -1025,38 +1034,38 @@ private MonitorInfo SelectedScreen() private double HorizonCenter(MonitorInfo screen) { - var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0); + var dip2 = screen.TransformPixelsToDIP(screen.WorkingArea.Width, 0); var left = (dip2.X - ActualWidth) / 2 + dip1.X; return left; } private double VerticalCenter(MonitorInfo screen) { - var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var dip1 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Y); + var dip2 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Height); var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y; return top; } private double HorizonRight(MonitorInfo screen) { - var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0); + var dip2 = screen.TransformPixelsToDIP(screen.WorkingArea.Width, 0); var left = (dip1.X + dip2.X - ActualWidth) - 10; return left; } private double HorizonLeft(MonitorInfo screen) { - var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0); var left = dip1.X + 10; return left; } private double VerticalTop(MonitorInfo screen) { - var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip1 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Y); var top = dip1.Y + 10; return top; } @@ -1185,7 +1194,7 @@ private void WindowAnimation() iconsb.Children.Add(IconMotion); iconsb.Children.Add(IconOpacity); - _settings.WindowLeft = Left; + SaveWindowPositionAndDisplayMetrics(); _isArrowKeyPressed = false; clocksb.Begin(ClockPanel); @@ -1449,23 +1458,23 @@ private void InitializeDialogJumpPosition() var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window); if (!result) return; + var monitor = MonitorInfo.GetNearestDisplayMonitor(_viewModel.DialogWindowHandle); + var dipWindow = monitor.TransformPixelsToDIP(window); + // Move window below the bottom of the dialog and keep it center - Top = VerticalBottom(window); - Left = HorizonCenter(window); + Top = VerticalBottom(dipWindow); + Left = HorizonCenter(dipWindow); } private double HorizonCenter(Rect window) { - var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0); - var dip2 = Win32Helper.TransformPixelsToDIP(this, window.Width, 0); - var left = (dip2.X - ActualWidth) / 2 + dip1.X; + var left = (window.Width - ActualWidth) / 2 + window.X; return left; } private double VerticalBottom(Rect window) { - var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, window.Bottom); - return dip1.Y; + return window.Bottom; } #endregion diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs index 054c60c8a7e..f15fd73e08b 100644 --- a/Flow.Launcher/SettingWindow.xaml.cs +++ b/Flow.Launcher/SettingWindow.xaml.cs @@ -179,8 +179,8 @@ private void SetWindowPosition(double top, double left) // Ensure window does not exceed screen boundaries top = Math.Max(top, SystemParameters.VirtualScreenTop); left = Math.Max(left, SystemParameters.VirtualScreenLeft); - top = Math.Min(top, SystemParameters.VirtualScreenHeight - ActualHeight); - left = Math.Min(left, SystemParameters.VirtualScreenWidth - ActualWidth); + top = Math.Min(top, SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - ActualHeight); + left = Math.Min(left, SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - ActualWidth); Top = top; Left = left; @@ -191,30 +191,26 @@ private void AdjustWindowPosition(ref double top, ref double left) // Adjust window position if it exceeds screen boundaries top = Math.Max(top, SystemParameters.VirtualScreenTop); left = Math.Max(left, SystemParameters.VirtualScreenLeft); - top = Math.Min(top, SystemParameters.VirtualScreenHeight - ActualHeight); - left = Math.Min(left, SystemParameters.VirtualScreenWidth - ActualWidth); + top = Math.Min(top, SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - ActualHeight); + left = Math.Min(left, SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - ActualWidth); } private static bool IsPositionValid(double top, double left) { - foreach (var screen in MonitorInfo.GetDisplayMonitors()) - { - var workingArea = screen.WorkingArea; - - if (left >= workingArea.Left && left < workingArea.Right && - top >= workingArea.Top && top < workingArea.Bottom) - { - return true; - } - } - return false; + // Use SystemParameters (DIP units) to match the coordinate system of Window.Top/Left. + // MonitorInfo.WorkingArea uses physical pixels which can differ from DIP units when DPI + // scaling is active, leading to incorrect results on high-DPI or mixed-DPI setups. + return left >= SystemParameters.VirtualScreenLeft && + left < SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth && + top >= SystemParameters.VirtualScreenTop && + top < SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight; } private double WindowLeft() { var screen = MonitorInfo.GetCursorDisplayMonitor(); - var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0); + var dip2 = screen.TransformPixelsToDIP(screen.WorkingArea.Width, 0); var left = (dip2.X - ActualWidth) / 2 + dip1.X; return left; } @@ -222,8 +218,8 @@ private double WindowLeft() private double WindowTop() { var screen = MonitorInfo.GetCursorDisplayMonitor(); - var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var dip1 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Y); + var dip2 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Height); var top = (dip2.Y - ActualHeight) / 2 + dip1.Y - 20; return top; }