Skip to content

Commit 6007f1a

Browse files
committed
Make 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.
1 parent 0c2d24c commit 6007f1a

File tree

5 files changed

+99
-7
lines changed

5 files changed

+99
-7
lines changed

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/DockSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public class DockSettings
2222

2323
public DockSize DockIconsSize { get; set; } = DockSize.Small;
2424

25+
public bool AlwaysOnTop { get; set; } = true;
26+
2527
// <Theme settings>
2628
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
2729

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,16 @@ public bool Dock_ShowLabels
227227
}
228228
}
229229

230+
public bool Dock_AlwaysOnTop
231+
{
232+
get => _settings.DockSettings.AlwaysOnTop;
233+
set
234+
{
235+
_settings.DockSettings.AlwaysOnTop = value;
236+
Save();
237+
}
238+
}
239+
230240
public bool EnableDock
231241
{
232242
get => _settings.EnableDock;

src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public sealed partial class DockWindow : WindowEx,
3939
IRecipient<QuitMessage>,
4040
IDisposable
4141
{
42+
private static readonly TimeSpan TopmostStateRefreshInterval = TimeSpan.FromSeconds(1);
43+
4244
#pragma warning disable SA1306 // Field names should begin with lower-case letter
4345
#pragma warning disable SA1310 // Field names should not contain underscore
4446
private readonly uint WM_TASKBAR_RESTART;
@@ -49,9 +51,13 @@ public sealed partial class DockWindow : WindowEx,
4951
private readonly DockWindowViewModel _windowViewModel;
5052
private readonly HiddenOwnerWindowBehavior _hiddenOwnerWindowBehavior = new();
5153

54+
// SHQueryUserNotificationState does not raise change notifications, so we poll lightly.
55+
private readonly DispatcherTimer _topmostStateTimer = new();
56+
5257
private HWND _hwnd = HWND.Null;
5358
private APPBARDATA _appBarData;
5459
private uint _callbackMessageId;
60+
private bool _isWindowTopmost;
5561

5662
private DockSettings _settings;
5763
private DockViewModel viewModel;
@@ -91,6 +97,8 @@ public DockWindow()
9197
}
9298

9399
this.Activated += DockWindow_Activated;
100+
_topmostStateTimer.Interval = TopmostStateRefreshInterval;
101+
_topmostStateTimer.Tick += TopmostStateTimer_Tick;
94102

95103
WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
96104
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
@@ -143,6 +151,13 @@ private void DockWindow_Activated(object sender, WindowActivatedEventArgs args)
143151
BOOL value = false;
144152
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
145153
}
154+
155+
UpdateTopmostState();
156+
}
157+
158+
private void TopmostStateTimer_Tick(object? sender, object e)
159+
{
160+
UpdateTopmostState();
146161
}
147162

148163
private HWND GetWindowHandle(Window window)
@@ -164,6 +179,18 @@ private void UpdateSettingsOnUiThread()
164179
}
165180

166181
_dock.UpdateSettings(_settings);
182+
183+
// Only poll for fullscreen changes when AlwaysOnTop is active;
184+
// when disabled the timer is unnecessary work.
185+
if (_settings.AlwaysOnTop)
186+
{
187+
_topmostStateTimer.Start();
188+
}
189+
else
190+
{
191+
_topmostStateTimer.Stop();
192+
}
193+
167194
var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);
168195

169196
if (_appBarData.hWnd != IntPtr.Zero)
@@ -172,13 +199,15 @@ private void UpdateSettingsOnUiThread()
172199
var sameSize = _lastSize == _settings.DockSize;
173200
if (sameEdge && sameSize)
174201
{
202+
UpdateTopmostState();
175203
return;
176204
}
177205

178206
DestroyAppBar(_hwnd);
179207
}
180208

181209
CreateAppBar(_hwnd);
210+
UpdateTopmostState();
182211
}
183212

184213
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
@@ -278,6 +307,40 @@ private void DestroyAppBar(HWND hwnd)
278307
_appBarData = default;
279308
}
280309

310+
private void UpdateTopmostState(bool bringToFront = false)
311+
{
312+
var shouldStayOnTop = _settings.AlwaysOnTop && !WindowHelper.IsWindowFullscreen();
313+
const SET_WINDOW_POS_FLAGS flags = SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE;
314+
315+
if (shouldStayOnTop)
316+
{
317+
if (_isWindowTopmost && !bringToFront)
318+
{
319+
return;
320+
}
321+
322+
PInvoke.SetWindowPos(_hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, flags);
323+
_isWindowTopmost = true;
324+
return;
325+
}
326+
327+
if (bringToFront)
328+
{
329+
// Win32 trick: briefly set HWND_TOPMOST then immediately clear it
330+
// with HWND_NOTOPMOST. This brings the window to the foreground
331+
// without permanently pinning it as topmost.
332+
PInvoke.SetWindowPos(_hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, flags);
333+
}
334+
335+
if (!_isWindowTopmost && !bringToFront)
336+
{
337+
return;
338+
}
339+
340+
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, flags);
341+
_isWindowTopmost = false;
342+
}
343+
281344
private void UpdateWindowPosition()
282345
{
283346
Logger.LogDebug("UpdateWindowPosition");
@@ -521,9 +584,7 @@ void IRecipient<BringToTopMessage>.Receive(BringToTopMessage message)
521584
{
522585
DispatcherQueue.TryEnqueue(() =>
523586
{
524-
var onTop = message.OnTop ? HWND.HWND_TOPMOST : HWND.HWND_NOTOPMOST;
525-
PInvoke.SetWindowPos(_hwnd, onTop, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
526-
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
587+
UpdateTopmostState(message.BringToFront);
527588
});
528589
}
529590

@@ -615,6 +676,8 @@ private void RequestShowPaletteOnUiThread(Point posDips)
615676

616677
public void Dispose()
617678
{
679+
_topmostStateTimer.Stop();
680+
_topmostStateTimer.Tick -= TopmostStateTimer_Tick;
618681
DisposeAcrylic();
619682
_windowViewModel.Dispose();
620683
}
@@ -625,6 +688,8 @@ private void DockWindow_Closed(object sender, WindowEventArgs args)
625688
var settings = serviceProvider.GetService<SettingsModel>();
626689
settings?.SettingsChanged -= SettingsChangedHandler;
627690
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
691+
_topmostStateTimer.Stop();
692+
_topmostStateTimer.Tick -= TopmostStateTimer_Tick;
628693
DisposeAcrylic();
629694

630695
// Remove our app bar registration
@@ -697,18 +762,20 @@ private static void WinEventCallback(
697762
if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND)
698763
{
699764
var @class = GetWindowClass(hwnd);
700-
if (string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal))
765+
var bringToFront = string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal);
766+
if (bringToFront)
701767
{
702768
Logger.LogDebug("ShowDesktop invoked. Bring us back");
703-
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(true));
704769
}
770+
771+
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(bringToFront));
705772
}
706773
}
707774

708775
public static bool IsHooked { get; private set; }
709776
}
710777

711-
internal sealed record BringToTopMessage(bool OnTop);
778+
internal sealed record BringToTopMessage(bool BringToFront);
712779

713780
internal sealed record RequestShowPaletteAtMessage(Point PosDips);
714781

src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/DockSettingsPage.xaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@
212212
</controls:SettingsExpander.Items>
213213
</controls:SettingsExpander>
214214

215+
<!-- Behavior Section -->
216+
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
217+
218+
<controls:SettingsCard x:Uid="DockBehavior_AlwaysOnTop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE840;}">
219+
<ToggleSwitch IsOn="{x:Bind ViewModel.Dock_AlwaysOnTop, Mode=TwoWay}" />
220+
</controls:SettingsCard>
221+
215222
<!-- Bands Section -->
216223
<!-- <TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
217224
@@ -246,4 +253,4 @@
246253
</Grid>
247254
</ScrollViewer>
248255
</Grid>
249-
</Page>
256+
</Page>

src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
434434
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve">
435435
<value>Enable a toolbar with quick access to commands</value>
436436
</data>
437+
<data name="DockBehavior_AlwaysOnTop_SettingsCard.Header" xml:space="preserve">
438+
<value>Always stay on top</value>
439+
</data>
440+
<data name="DockBehavior_AlwaysOnTop_SettingsCard.Description" xml:space="preserve">
441+
<value>Keep the dock above other windows, except while an app is fullscreen</value>
442+
</data>
437443
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
438444
<value>Back</value>
439445
</data>

0 commit comments

Comments
 (0)