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:
Jiří Polášek
2025-11-24 23:57:10 +01:00
committed by GitHub
parent 09c8c1d79a
commit 2c9a9e9fca
2 changed files with 161 additions and 23 deletions

View File

@@ -2,21 +2,52 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using Windows.Graphics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.UI.ViewModels; namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class WindowPosition public sealed class WindowPosition
{ {
/// <summary>
/// Gets or sets left position in device pixels.
/// </summary>
public int X { get; set; } public int X { get; set; }
/// <summary>
/// Gets or sets top position in device pixels.
/// </summary>
public int Y { get; set; } public int Y { get; set; }
/// <summary>
/// Gets or sets width in device pixels.
/// </summary>
public int Width { get; set; } public int Width { get; set; }
/// <summary>
/// Gets or sets height in device pixels.
/// </summary>
public int Height { get; set; } 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);
}
} }

View File

@@ -18,6 +18,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops; using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input; using Microsoft.UI.Input;
@@ -33,6 +34,8 @@ using Windows.UI.WindowManagement;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm; 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.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging; using Windows.Win32.UI.WindowsAndMessaging;
using WinRT; using WinRT;
@@ -48,6 +51,9 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<QuitMessage>, IRecipient<QuitMessage>,
IDisposable 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", "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_")] [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 uint WM_TASKBAR_RESTART;
@@ -173,22 +179,8 @@ public sealed partial class MainWindow : WindowEx,
return; return;
} }
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height }); var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
AppWindow.MoveAndResize(newRect);
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);
} }
private void PositionCentered(DisplayArea displayArea) private void PositionCentered(DisplayArea displayArea)
@@ -207,12 +199,16 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateWindowPositionInMemory() private void UpdateWindowPositionInMemory()
{ {
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
_currentWindowPosition = new WindowPosition _currentWindowPosition = new WindowPosition
{ {
X = AppWindow.Position.X, X = AppWindow.Position.X,
Y = AppWindow.Position.Y, Y = AppWindow.Position.Y,
Width = AppWindow.Size.Width, Width = AppWindow.Size.Width,
Height = AppWindow.Size.Height, 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) if (target == MonitorBehavior.ToLast)
{ {
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height }); var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y }); AppWindow.MoveAndResize(newRect);
} }
else 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); 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) private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
{ {
// Leaving a note here, in case we ever need it: // Leaving a note here, in case we ever need it:
@@ -479,6 +583,9 @@ public sealed partial class MainWindow : WindowEx,
Y = _currentWindowPosition.Y, Y = _currentWindowPosition.Y,
Width = _currentWindowPosition.Width, Width = _currentWindowPosition.Width,
Height = _currentWindowPosition.Height, Height = _currentWindowPosition.Height,
Dpi = _currentWindowPosition.Dpi,
ScreenWidth = _currentWindowPosition.ScreenWidth,
ScreenHeight = _currentWindowPosition.ScreenHeight,
}; };
SettingsModel.SaveSettings(settings); SettingsModel.SaveSettings(settings);