CmdPal: Make Dock stay on top of all other windows (#46163)

<!-- 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

This PR makes Dock stay on top of all other windows:

- Marks the Dock window as topmost when there is no full-screen window.
- Adds a new option that allows the user to disable this behavior.
- Adds a timer that polls the system API to determine whether a
full-screen window is present.

## Pictures? Pictures!

<img width="560" height="283" alt="image"
src="https://github.com/user-attachments/assets/55346005-2fac-4357-88bd-60c899565fac"
/>



https://github.com/user-attachments/assets/b81bff6d-4616-4d17-a1b0-063d254022ed


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #46161
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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
2026-03-30 19:11:40 +02:00
committed by GitHub
parent 0b7d780980
commit a022a9f024
7 changed files with 82 additions and 7 deletions

View File

@@ -339,6 +339,7 @@ SETAUTOHIDEBAR
WINDOWPOS WINDOWPOS
WINEVENTPROC WINEVENTPROC
WORKERW WORKERW
FULLSCREENAPP
# PowerRename metadata pattern abbreviations (used in tests and regex patterns) # PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD DDDD

View File

@@ -22,6 +22,8 @@ public record DockSettings
public DockSize DockIconsSize { get; init; } = DockSize.Small; public DockSize DockIconsSize { get; init; } = DockSize.Small;
public bool AlwaysOnTop { get; set; } = true;
// <Theme settings> // <Theme settings>
public DockBackdrop Backdrop { get; init; } = DockBackdrop.Acrylic; public DockBackdrop Backdrop { get; init; } = DockBackdrop.Acrylic;

View File

@@ -226,6 +226,15 @@ public partial class SettingsViewModel : INotifyPropertyChanged
} }
} }
public bool Dock_AlwaysOnTop
{
get => _settingsService.Settings.DockSettings.AlwaysOnTop;
set
{
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { AlwaysOnTop = value } });
}
}
public bool EnableDock public bool EnableDock
{ {
get => _settingsService.Settings.EnableDock; get => _settingsService.Settings.EnableDock;

View File

@@ -5,6 +5,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock; using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -51,6 +52,8 @@ public sealed partial class DockWindow : WindowEx,
private HWND _hwnd = HWND.Null; private HWND _hwnd = HWND.Null;
private APPBARDATA _appBarData; private APPBARDATA _appBarData;
private uint _callbackMessageId; private uint _callbackMessageId;
private bool _isWindowTopmost;
private bool _isFullScreenAppOpen;
private DockSettings _settings; private DockSettings _settings;
private DockViewModel viewModel; private DockViewModel viewModel;
@@ -129,6 +132,7 @@ public sealed partial class DockWindow : WindowEx,
_ = PInvoke.SetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style); _ = PInvoke.SetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style);
ShowDesktop.AddHook(this); ShowDesktop.AddHook(this);
_isFullScreenAppOpen = WindowHelper.IsWindowFullscreen();
UpdateSettingsOnUiThread(); UpdateSettingsOnUiThread();
} }
@@ -147,6 +151,8 @@ public sealed partial class DockWindow : WindowEx,
BOOL value = false; BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL)); PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
} }
UpdateTopmostState();
} }
private HWND GetWindowHandle(Window window) private HWND GetWindowHandle(Window window)
@@ -161,6 +167,7 @@ public sealed partial class DockWindow : WindowEx,
UpdateBackdrop(); UpdateBackdrop();
_dock.UpdateSettings(_settings); _dock.UpdateSettings(_settings);
var side = DockSettingsToViews.GetAppBarEdge(_settings.Side); var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);
if (_appBarData.hWnd != IntPtr.Zero) if (_appBarData.hWnd != IntPtr.Zero)
@@ -169,6 +176,7 @@ public sealed partial class DockWindow : WindowEx,
var sameSize = _lastSize == _settings.DockSize; var sameSize = _lastSize == _settings.DockSize;
if (sameEdge && sameSize) if (sameEdge && sameSize)
{ {
UpdateTopmostState();
return; return;
} }
@@ -176,6 +184,7 @@ public sealed partial class DockWindow : WindowEx,
} }
CreateAppBar(_hwnd); CreateAppBar(_hwnd);
UpdateTopmostState();
} }
private void InitializeBackdropSupport() private void InitializeBackdropSupport()
@@ -333,6 +342,41 @@ public sealed partial class DockWindow : WindowEx,
_appBarData = default; _appBarData = default;
} }
private void UpdateTopmostState(bool bringToFront = false)
{
var shouldStayOnTop = _settings.AlwaysOnTop && !_isFullScreenAppOpen;
const SET_WINDOW_POS_FLAGS flags = SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE;
if (shouldStayOnTop)
{
if (_isWindowTopmost && !bringToFront)
{
return;
}
PInvoke.SetWindowPos(_hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, flags);
_isWindowTopmost = true;
return;
}
if (bringToFront)
{
// Win32 trick: briefly set HWND_TOPMOST then immediately clear it
// with HWND_NOTOPMOST. This brings the window to the foreground
// without permanently pinning it as topmost.
PInvoke.SetWindowPos(_hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, flags);
}
if (!_isWindowTopmost && !bringToFront)
{
return;
}
var zOrder = _isFullScreenAppOpen ? HWND.HWND_BOTTOM : HWND.HWND_NOTOPMOST;
PInvoke.SetWindowPos(_hwnd, zOrder, 0, 0, 0, 0, flags);
_isWindowTopmost = false;
}
private void UpdateWindowPosition() private void UpdateWindowPosition()
{ {
Logger.LogDebug("UpdateWindowPosition"); Logger.LogDebug("UpdateWindowPosition");
@@ -558,6 +602,11 @@ public sealed partial class DockWindow : WindowEx,
{ {
UpdateWindowPosition(); UpdateWindowPosition();
} }
else if (wParam.Value == PInvoke.ABN_FULLSCREENAPP)
{
_isFullScreenAppOpen = lParam != 0;
UpdateTopmostState();
}
} }
else if (msg == WM_TASKBAR_RESTART) else if (msg == WM_TASKBAR_RESTART)
{ {
@@ -576,9 +625,7 @@ public sealed partial class DockWindow : WindowEx,
{ {
DispatcherQueue.TryEnqueue(() => DispatcherQueue.TryEnqueue(() =>
{ {
var onTop = message.OnTop ? HWND.HWND_TOPMOST : HWND.HWND_NOTOPMOST; UpdateTopmostState(message.BringToFront);
PInvoke.SetWindowPos(_hwnd, onTop, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}); });
} }
@@ -770,18 +817,20 @@ internal static class ShowDesktop
if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND) if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND)
{ {
var @class = GetWindowClass(hwnd); var @class = GetWindowClass(hwnd);
if (string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal)) var bringToFront = string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal);
if (bringToFront)
{ {
Logger.LogDebug("ShowDesktop invoked. Bring us back"); Logger.LogDebug("ShowDesktop invoked. Bring us back");
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(true));
} }
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(bringToFront));
} }
} }
public static bool IsHooked { get; private set; } public static bool IsHooked { get; private set; }
} }
internal sealed record BringToTopMessage(bool OnTop); internal sealed record BringToTopMessage(bool BringToFront);
internal sealed record RequestShowPaletteAtMessage(Point PosDips); internal sealed record RequestShowPaletteAtMessage(Point PosDips);

View File

@@ -76,6 +76,7 @@ ABM_REMOVE
ABM_SETAUTOHIDEBAR ABM_SETAUTOHIDEBAR
ABS_AUTOHIDE ABS_AUTOHIDE
ABN_POSCHANGED ABN_POSCHANGED
ABN_FULLSCREENAPP
APPBARDATA APPBARDATA
ABE_TOP ABE_TOP
ABE_BOTTOM ABE_BOTTOM

View File

@@ -212,6 +212,13 @@
</controls:SettingsExpander.Items> </controls:SettingsExpander.Items>
</controls:SettingsExpander> </controls:SettingsExpander>
<!-- Behavior Section -->
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="DockBehavior_AlwaysOnTop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE840;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.Dock_AlwaysOnTop, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Bands Section --> <!-- Bands Section -->
<!-- <TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <!-- <TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -246,4 +253,4 @@
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</Page> </Page>

View File

@@ -464,6 +464,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve"> <data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve">
<value>Enable a toolbar with quick access to commands</value> <value>Enable a toolbar with quick access to commands</value>
</data> </data>
<data name="DockBehavior_AlwaysOnTop_SettingsCard.Header" xml:space="preserve">
<value>Always stay on top</value>
</data>
<data name="DockBehavior_AlwaysOnTop_SettingsCard.Description" xml:space="preserve">
<value>Keep the dock above other windows, except while an app is fullscreen</value>
</data>
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Back</value> <value>Back</value>
</data> </data>