mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
CmdPal: Improve Command Palette behavior in "Last position" mode (#43543)
## Summary of the Pull Request This PR improves Command Palette behavior in “Last position” mode: - Correctly handles DPI changes between monitors. - Ensures the window is always visible — if it’s fully off-screen or has less than 100px visible on any axis, it is re-centered. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #43398 - [ ] **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 <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -2,21 +2,52 @@
|
||||
// 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class WindowPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets left position in device pixels.
|
||||
/// </summary>
|
||||
public int X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets top position in device pixels.
|
||||
/// </summary>
|
||||
public int Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width in device pixels.
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height in device pixels.
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets DPI (dots per inch) of the display where the window is located.
|
||||
/// </summary>
|
||||
public int Dpi { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
|
||||
/// </summary>
|
||||
public RectInt32 ToPhysicalWindowRectangle()
|
||||
{
|
||||
return new RectInt32(X, Y, Width, Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Input;
|
||||
@@ -33,6 +34,8 @@ 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;
|
||||
@@ -48,6 +51,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<QuitMessage>,
|
||||
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;
|
||||
@@ -173,22 +179,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
return;
|
||||
}
|
||||
|
||||
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
|
||||
|
||||
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
|
||||
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
|
||||
var workArea = displayArea.WorkArea;
|
||||
|
||||
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
|
||||
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
|
||||
|
||||
var targetPoint = new PointInt32
|
||||
{
|
||||
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
|
||||
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
|
||||
};
|
||||
|
||||
AppWindow.Move(targetPoint);
|
||||
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
|
||||
private void PositionCentered(DisplayArea displayArea)
|
||||
@@ -207,12 +199,16 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -300,8 +296,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
|
||||
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
|
||||
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -330,6 +326,114 @@ public sealed partial class MainWindow : WindowEx,
|
||||
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:
|
||||
@@ -479,6 +583,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
Y = _currentWindowPosition.Y,
|
||||
Width = _currentWindowPosition.Width,
|
||||
Height = _currentWindowPosition.Height,
|
||||
Dpi = _currentWindowPosition.Dpi,
|
||||
ScreenWidth = _currentWindowPosition.ScreenWidth,
|
||||
ScreenHeight = _currentWindowPosition.ScreenHeight,
|
||||
};
|
||||
|
||||
SettingsModel.SaveSettings(settings);
|
||||
|
||||
Reference in New Issue
Block a user