Compare commits

...

2 Commits

Author SHA1 Message Date
Noraa Junker
2533c83f1b background shortcut guide 2026-06-26 22:32:23 +02:00
Niels Laute
b2f5d1d1dd Shortcut Guide: Replace dual windows with single transparent overlay
Refactor Shortcut Guide UI from two separate WindowEx instances (MainWindow
+ TaskbarWindow) to a single full-monitor transparent OverlayWindow that
hosts both surfaces as XAML UserControls (pseudo-windows).

Key changes:
- New OverlayWindow with TransparentTintBackdrop as the host
- MainPaneControl: main shortcut list with per-card acrylic backdrop
- TaskbarPaneControl + TaskbarIndicator: tooltip-style indicators with
  triangle tails, positioned above taskbar buttons
- Robust multi-monitor DPI handling (WM_DPICHANGED suppression pattern)
- Win11 phantom border elimination (7-layer DWM/style stripping)
- Windows 11 system flyout animations (slide + fade, direction-aware
  based on left/right position setting, ~367ms entrance, ~200ms exit)
- Close button on main flyout title bar
- Click-outside-to-close with animated exit transition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-16 19:42:01 +02:00
28 changed files with 1890 additions and 572 deletions

View File

@@ -52,7 +52,7 @@ namespace ShortcutGuide.Converters
if (description.Shift)
{
shortcutList.Add(16); // The Shift key or button.
}
}
foreach (var key in description.Keys)
{

View File

@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation
// 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.Text;
using System.Threading;
using ManagedCommon;
namespace ShortcutGuide.Helpers
{
/// <summary>
/// Helper class for Windows named event operations.
/// Provides unified event signaling with consistent error handling and logging.
/// </summary>
public static class EventHelper
{
/// <summary>
/// Signals a named event. Creates the event if it doesn't exist.
/// </summary>
/// <param name="eventName">The name of the event to signal.</param>
/// <returns>True if the event was signaled successfully, false otherwise.</returns>
public static bool SignalEvent(string eventName)
{
if (string.IsNullOrEmpty(eventName))
{
Logger.LogWarning("[EventHelper] SignalEvent called with null or empty event name");
return false;
}
try
{
using var eventHandle = new EventWaitHandle(
false,
EventResetMode.AutoReset,
eventName);
eventHandle.Set();
return true;
}
catch (Exception ex)
{
Logger.LogError($"[EventHelper] Failed to signal event '{eventName}': {ex.Message}");
return false;
}
}
}
}

View File

@@ -46,6 +46,7 @@ namespace ShortcutGuide.Helpers
stream.Position = 0;
BitmapImage bitmapImage = new();
bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmapImage.SetSource(stream.AsRandomAccessStream());
return bitmapImage;
}

View File

@@ -19,7 +19,7 @@ namespace ShortcutGuide.Helpers
/// <returns>An array of the taskbar buttons.</returns>
public static TasklistButton[] GetButtons()
{
var monitor = NativeMethods.MonitorFromWindow(WindowNative.GetWindowHandle(App.MainWindow), 0);
var monitor = NativeMethods.MonitorFromWindow(WindowNative.GetWindowHandle(App.OverlayWindow), 0);
nint ptr = NativeMethods.GetTasklistButtons(monitor, out int size);
if (ptr == nint.Zero)
{

View File

@@ -11,7 +11,12 @@ namespace ShortcutGuide;
internal static partial class NativeMethods
{
internal const int GWL_STYLE = -16;
internal const int GWL_EXSTYLE = -20;
internal const int WS_CAPTION = 0x00C00000;
internal const int WS_EX_TOOLWINDOW = 0x00000080;
internal const int WS_EX_WINDOWEDGE = 0x00000100;
internal const int WS_EX_CLIENTEDGE = 0x00000200;
internal const int WS_EX_DLGMODALFRAME = 0x00000001;
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -29,6 +34,13 @@ internal static partial class NativeMethods
[LibraryImport("User32.dll")]
internal static partial IntPtr MonitorFromWindow(IntPtr hwnd, int dwFlags);
[LibraryImport("User32.dll")]
internal static partial IntPtr MonitorFromPoint(POINT pt, int dwFlags);
[LibraryImport("User32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorInfoW(IntPtr hMonitor, ref MONITORINFO lpmi);
[LibraryImport("Shcore.dll")]
internal static partial long GetDpiForMonitor(IntPtr hmonitor, int dpiType, ref int dpiX, ref int dpiY);
@@ -58,6 +70,58 @@ internal static partial class NativeMethods
internal delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData);
[LibraryImport("user32.dll")]
internal static partial void CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, LPARAM lParam);
[DllImport("user32.dll")]
internal static extern void SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs, int cbSize);
internal struct INPUT
{
public uint Type;
public MOUSEKEYBDHARDWAREINPUT Data;
}
[StructLayout(LayoutKind.Explicit)]
internal struct MOUSEKEYBDHARDWAREINPUT
{
[FieldOffset(0)]
public MOUSEINPUT Mouse;
[FieldOffset(0)]
public KEYBDINPUT Keyboard;
[FieldOffset(0)]
public HARDWAREINPUT Hardware;
}
[StructLayout(LayoutKind.Sequential)]
internal struct MOUSEINPUT
{
public int Dx;
public int Dy;
public uint MouseData;
public uint DwFlags;
public uint Time;
public IntPtr DwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
internal struct KEYBDINPUT
{
public ushort WVk;
public ushort WScan;
public uint DwFlags;
public uint Time;
public IntPtr DwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
internal struct HARDWAREINPUT
{
public uint Msg;
public ushort ParamL;
public ushort ParamH;
}
internal struct LPARAM(IntPtr value)
{
internal IntPtr Value = value;
@@ -103,6 +167,15 @@ internal static partial class NativeMethods
}
}
[StructLayout(LayoutKind.Sequential)]
internal struct MONITORINFO
{
public uint CbSize;
public RECT RcMonitor;
public RECT RcWork;
public uint DwFlags;
}
public enum MonitorFromWindowDwFlags
{
MONITOR_DEFAULTTONEAREST = 2,

View File

@@ -21,13 +21,14 @@ namespace ShortcutGuide
{
public static Thread CopyAndIndexGenerationThread { get; private set; } = null!;
public static nint ForegroundWindowHandle { get; private set; } = nint.Zero;
public static nint ForegroundWindowHandle { get; set; } = nint.Zero;
[STAThread]
public static void Main(string[] args)
{
ForegroundWindowHandle = NativeMethods.GetForegroundWindow();
Logger.InitializeLogger("\\ShortcutGuide\\Logs");
LogForegroundCapture(ForegroundWindowHandle);
// The module interface passes: <powertoys_pid> [telemetry]
if (args.Length >= 2 && args[1] == "telemetry")
@@ -156,5 +157,47 @@ namespace ShortcutGuide
Logger.LogError("Failed to send settings telemetry.", ex);
}
}
/// <summary>
/// Logs the foreground window captured at startup so we can see in
/// the SG log which app was foreground at the moment SG.exe began
/// running. The dictionary populated from this HWND drives the order
/// of nav items, so a wrong/empty capture surfaces as the wrong app
/// being auto-selected.
/// </summary>
private static void LogForegroundCapture(nint hwnd)
{
try
{
if (hwnd == nint.Zero)
{
Logger.LogInfo("Foreground capture: HWND=0 (no foreground window).");
return;
}
if (NativeMethods.GetWindowThreadProcessId(hwnd, out uint processId) == 0)
{
Logger.LogInfo($"Foreground capture: HWND=0x{hwnd:X}; GetWindowThreadProcessId failed.");
return;
}
string moduleName;
try
{
using var proc = Process.GetProcessById((int)processId);
moduleName = proc.MainModule?.ModuleName ?? "(null)";
}
catch (Exception ex)
{
moduleName = $"(failed: {ex.GetType().Name})";
}
Logger.LogInfo($"Foreground capture: HWND=0x{hwnd:X}, PID={processId}, Module={moduleName}");
}
catch (Exception ex)
{
Logger.LogError("Failed to log foreground capture.", ex);
}
}
}
}

View File

@@ -29,10 +29,13 @@
</ItemGroup>
<ItemGroup>
<None Remove="ShortcutGuideXAML\Controls\BlankPage.xaml" />
<None Remove="ShortcutGuideXAML\CustomNavigationViewStyle.xaml" />
<None Remove="ShortcutGuideXAML\Pages\ShortcutsPage.xaml" />
<None Remove="ShortcutGuideXAML\TaskbarIndicator.xaml" />
<None Remove="ShortcutGuideXAML\TaskbarWindow.xaml" />
<None Remove="ShortcutGuideXAML\OverlayWindow.xaml" />
<None Remove="ShortcutGuideXAML\Controls\MainPaneControl.xaml" />
<None Remove="ShortcutGuideXAML\Controls\TaskbarPaneControl.xaml" />
<None Include="Assets\ShortcutGuide\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@@ -110,7 +113,17 @@
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="ShortcutGuideXAML\TaskbarWindow.xaml">
<Page Update="ShortcutGuideXAML\OverlayWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="ShortcutGuideXAML\Controls\MainPaneControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="ShortcutGuideXAML\Controls\TaskbarPaneControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
@@ -127,6 +140,11 @@
<ItemGroup>
<Folder Include="ShortcutGuideXAML\Styles\" />
</ItemGroup>
<ItemGroup>
<Page Update="ShortcutGuideXAML\Controls\BlankPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if

View File

@@ -2,19 +2,28 @@
// 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.IO;
using System.Reflection.Metadata.Ecma335;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading;
using System.Windows.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using PowerToys.Interop;
using ShortcutGuide.Models;
using ShortcutGuide.ShortcutGuideXAML;
using ShortcutGuide.Telemetry;
using KeyEventHandler = Microsoft.UI.Xaml.Input.KeyEventHandler;
namespace ShortcutGuide
{
public partial class App
public partial class App : IDisposable
{
internal static Dictionary<string, List<ShortcutEntry>> PinnedShortcuts { get; private set; } = new Dictionary<string, List<ShortcutEntry>>();
@@ -22,12 +31,23 @@ namespace ShortcutGuide
internal static ShortcutGuideProperties ShortcutGuideProperties { get; private set; } = null!;
internal static MainWindow MainWindow { get; private set; } = null!;
/// <summary>
/// The single transparent host that replaces the previous MainWindow +
/// TaskbarWindow pair. The two surfaces are now XAML pseudo-windows
/// inside this one window.
/// </summary>
internal static OverlayWindow OverlayWindow { get; private set; } = null!;
internal static TaskbarWindow TaskBarWindow { get; private set; } = null!;
private HotkeySettingsControlHook _winKeyUpKeyboardHook = null!;
internal static string CurrentAppName { get; set; } = string.Empty;
private EventWaitHandle? _launchedEvent;
private Thread? _listenForLaunchedEventThread;
private static readonly UIntPtr _ignoreKeyEventFlag = 0x5557;
public App()
{
this.InitializeComponent();
@@ -35,17 +55,169 @@ namespace ShortcutGuide
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
this. LoadData();
OverlayWindow = new OverlayWindow();
OverlayWindow.Activate();
OverlayWindow.AppWindow.Hide();
OverlayWindow.Closed += (_, _) =>
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
OverlayWindow.SessionDurationMs,
OverlayWindow.CloseType));
// WinUI3's dispatcher loop does not terminate when the last
// window closes; without Exit() the SG.exe process stays
// alive, holds the AppInstance single-instance lock, and
// blocks the next launch (the well-known "every other
// long-press works" bug).
Current.Exit();
};
try
{
_launchedEvent = EventWaitHandle.OpenExisting(Constants.ShortcutGuideTriggerEvent());
}
catch (Exception ex)
{
Logger.LogError($"Failed to open existing event '{Constants.ShortcutGuideTriggerEvent()}': {ex.Message}");
}
_listenForLaunchedEventThread = new Thread(ListenForLaunchedEvents)
{
IsBackground = true,
Name = "ShortcutGuide-ShowEventListener",
};
_listenForLaunchedEventThread.Start();
_winKeyUpKeyboardHook = new HotkeySettingsControlHook(
(int key) =>
{
SendSingleKeyboardInput((short)key, 0x0); // key down
},
(int key) =>
{
if (OverlayWindow.AppWindow.IsVisible)
{
OverlayWindow.DispatcherQueue.TryEnqueue(() =>
{
OverlayWindow.CloseAnimated();
});
NativeMethods.SendInput(1, [new() { Type = 1, Data = new() { Keyboard = new NativeMethods.KEYBDINPUT { WVk = 0xFF, DwFlags = 0x2 } } }], Marshal.SizeOf<NativeMethods.INPUT>());
SendSingleKeyboardInput((short)key, 0x2); // key up
}
else
{
SendSingleKeyboardInput((short)key, 0x2); // key up
}
},
() => true,
(int key, nuint specialFlags) => key == 91 && specialFlags != _ignoreKeyEventFlag);
}
private static bool IsExtendedVirtualKey(short vk)
{
return vk switch
{
0xA5 => true, // VK_RMENU (Right Alt - AltGr)
0xA3 => true, // VK_RCONTROL
0x2D => true, // VK_INSERT
0x2E => true, // VK_DELETE
0x23 => true, // VK_END
0x24 => true, // VK_HOME
0x21 => true, // VK_PRIOR (Page Up)
0x22 => true, // VK_NEXT (Page Down)
0x90 => true, // VK_NUMLOCK
_ => false,
};
}
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
{
if (IsExtendedVirtualKey(keyCode))
{
keyStatus |= 0x1; // KEYEVENTF_EXTENDEDKEY
}
NativeMethods.INPUT input = new()
{
Type = 0x1, // INPUT_KEYBOARD
Data = new NativeMethods.MOUSEKEYBDHARDWAREINPUT
{
Keyboard = new NativeMethods.KEYBDINPUT
{
WVk = (ushort)keyCode,
DwFlags = keyStatus,
DwExtraInfo = (nint)_ignoreKeyEventFlag,
},
},
};
NativeMethods.INPUT[] inputs = [input];
NativeMethods.SendInput(1, inputs, Marshal.SizeOf<NativeMethods.INPUT>());
}
private void ListenForLaunchedEvents()
{
if (_launchedEvent == null)
{
return;
}
var handles = new WaitHandle[] { _launchedEvent };
try
{
while (true)
{
var index = WaitHandle.WaitAny(handles);
if (index == 0)
{
OverlayWindow.DispatcherQueue.TryEnqueue(async () =>
{
if (Keyboard.IsKeyDown(Key.LWin))
{
if (OverlayWindow.AppWindow.IsVisible)
{
return;
}
OverlayWindow.AppWindow.Show();
OverlayWindow.MainPaneControl.Visibility = Visibility.Collapsed;
OverlayWindow.AppWindow.MoveInZOrderAtTop();
OverlayWindow.UpdateTaskbarPaneLayout();
OverlayWindow.TaskbarPaneControl.Visibility = Visibility.Visible;
return;
}
if (OverlayWindow.AppWindow.IsVisible)
{
OverlayWindow.CloseAnimated();
OverlayWindow.MainPaneControl.Hide();
}
else
{
Program.ForegroundWindowHandle = NativeMethods.GetForegroundWindow();
OverlayWindow.MainPaneControl.Visibility = Visibility.Collapsed;
OverlayWindow.AppWindow.Show();
await OverlayWindow.MainPaneControl.Open();
OverlayWindow.AppWindow.MoveInZOrderAtTop();
OverlayWindow.UpdateTaskbarPaneLayout();
OverlayWindow.MainPaneControl.Visibility = Visibility.Visible;
}
});
}
else
{
break;
}
}
}
catch (ObjectDisposedException)
{
}
catch (ThreadInterruptedException)
{
}
}
private void LoadData()
@@ -76,5 +248,33 @@ namespace ShortcutGuide
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
}
public void Dispose()
{
_launchedEvent?.Dispose();
if (_listenForLaunchedEventThread == null)
{
return;
}
try
{
if (!_listenForLaunchedEventThread.Join(TimeSpan.FromMilliseconds(250)))
{
_listenForLaunchedEventThread.Interrupt();
_listenForLaunchedEventThread.Join(TimeSpan.FromMilliseconds(250));
}
}
catch (ThreadInterruptedException)
{
}
catch (ThreadStateException)
{
}
_listenForLaunchedEventThread = null;
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation
// 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.Collections.Generic;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
namespace ShortcutGuide.Backdrops;
/// <summary>
/// A <see cref="SystemBackdrop"/> that renders desktop acrylic and stays in
/// the active visual state even when the hosting window is not activated.
/// </summary>
/// <remarks>
/// The built-in <see cref="DesktopAcrylicBackdrop"/> tracks the host window's
/// <c>IsInputActive</c> state and falls back to a solid color whenever the
/// window is not the foreground window. That makes it unusable for the
/// per-card backdrops inside the Shortcut Guide overlay: the overlay window
/// is technically the foreground, but the <c>SystemBackdropElement</c>s that
/// host the per-pane acrylic are themselves not activated, and the standard
/// backdrop greys out anyway when the overlay loses focus during the brief
/// hand-off to elevated windows / context menus.
///
/// This backdrop drives a <see cref="DesktopAcrylicController"/> with a
/// <see cref="SystemBackdropConfiguration"/> whose <c>IsInputActive</c> is
/// permanently <see langword="true"/>, so the native acrylic effect is always
/// rendered.
///
/// Mirrors the implementation in PR #48176 (CmdPal toast notification). Once
/// that PR lands and the type is promoted into <c>Common.UI.Controls</c>,
/// this local copy should be retired in favor of the shared one.
/// </remarks>
public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
{
private readonly Dictionary<ICompositionSupportsSystemBackdrop, BackdropTarget> _targets = new();
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
{
base.OnTargetConnected(connectedTarget, xamlRoot);
var configuration = new SystemBackdropConfiguration
{
IsInputActive = true,
Theme = ResolveTheme(xamlRoot),
};
var controller = new DesktopAcrylicController();
controller.SetSystemBackdropConfiguration(configuration);
controller.AddSystemBackdropTarget(connectedTarget);
var target = new BackdropTarget(controller, configuration, xamlRoot);
_targets[connectedTarget] = target;
if (xamlRoot.Content is FrameworkElement rootElement)
{
rootElement.ActualThemeChanged += target.OnActualThemeChanged;
}
}
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
{
base.OnTargetDisconnected(disconnectedTarget);
if (_targets.Remove(disconnectedTarget, out var target))
{
if (target.XamlRoot.Content is FrameworkElement rootElement)
{
rootElement.ActualThemeChanged -= target.OnActualThemeChanged;
}
target.Controller.RemoveSystemBackdropTarget(disconnectedTarget);
target.Controller.Dispose();
}
}
private static SystemBackdropTheme ResolveTheme(XamlRoot xamlRoot) =>
xamlRoot.Content is FrameworkElement rootElement
? rootElement.ActualTheme switch
{
ElementTheme.Dark => SystemBackdropTheme.Dark,
ElementTheme.Light => SystemBackdropTheme.Light,
_ => SystemBackdropTheme.Default,
}
: SystemBackdropTheme.Default;
private sealed class BackdropTarget
{
public BackdropTarget(DesktopAcrylicController controller, SystemBackdropConfiguration configuration, XamlRoot xamlRoot)
{
Controller = controller;
Configuration = configuration;
XamlRoot = xamlRoot;
}
public DesktopAcrylicController Controller { get; }
public SystemBackdropConfiguration Configuration { get; }
public XamlRoot XamlRoot { get; }
public void OnActualThemeChanged(FrameworkElement sender, object args)
{
Configuration.Theme = ResolveTheme(XamlRoot);
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="ShortcutGuide.ShortcutGuideXAML.Controls.BlankPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ShortcutGuide.ShortcutGuideXAML.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<!-- Blank space to save memory -->
</Grid>
</Page>

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml.Controls;
namespace ShortcutGuide.ShortcutGuideXAML.Controls
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class BlankPage : Page
{
public BlankPage()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encod ing="utf-8" ?>
<UserControl
x:Class="ShortcutGuide.Controls.MainPaneControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:backdrops="using:ShortcutGuide.Backdrops"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<!--
Pseudo-window: chrome (per-card acrylic + border + corner radius + shadow)
is provided here in XAML instead of by a real WindowEx host. The outer Grid
carries Translation Z=24 so its ThemeShadow projects onto the transparent
overlay window behind it.
-->
<Grid
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Translation="0,0,24">
<Grid.Shadow>
<ThemeShadow />
</Grid.Shadow>
<SystemBackdropElement CornerRadius="8">
<SystemBackdropElement.SystemBackdrop>
<backdrops:AlwaysActiveDesktopAcrylicBackdrop />
</SystemBackdropElement.SystemBackdrop>
</SystemBackdropElement>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
Padding="16,0"
ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image
Width="16"
Height="16"
VerticalAlignment="Center"
Source="/Assets/ShortcutGuide/ShortcutGuide.ico" />
<TextBlock
x:Name="TitleTextBlock"
Grid.Column="1"
VerticalAlignment="Center"
Style="{ThemeResource CaptionTextBlockStyle}" />
<Button
x:Name="CloseButton"
x:Uid="CloseButton"
Grid.Column="2"
Width="32"
Height="32"
Padding="0"
VerticalAlignment="Center"
Style="{StaticResource SubtleButtonStyle}"
Click="CloseButton_Click">
<FontIcon FontSize="12" Glyph="&#xE711;" />
</Button>
</Grid>
<NavigationView
x:Name="WindowSelector"
Grid.Row="1"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
SelectionChanged="WindowSelector_SelectionChanged"
Style="{StaticResource RailNavigationViewStyle}">
<NavigationView.MenuItems />
<NavigationView.FooterMenuItems>
<NavigationViewItem
x:Uid="SettingsButton"
Icon="Setting"
SelectsOnInvoked="False"
Tag="Settings"
Tapped="Settings_Tapped" />
</NavigationView.FooterMenuItems>
<NavigationView.Content>
<Frame
x:Name="ContentFrame"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1,1,0,0"
CornerRadius="8,0,0,0" />
</NavigationView.Content>
<NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
</NavigationView.Resources>
</NavigationView>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,271 @@
// Copyright (c) Microsoft Corporation
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Common.UI;
using ManagedCommon;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using ShortcutGuide.Helpers;
using ShortcutGuide.Models;
using ShortcutGuide.Pages;
using ShortcutGuide.ShortcutGuideXAML.Controls;
namespace ShortcutGuide.Controls
{
/// <summary>
/// The big shortcut-list pseudo-window that used to be <c>MainWindow</c>.
/// Now a regular <see cref="UserControl"/> hosted inside
/// <see cref="OverlayWindow"/>, so it can be sized / positioned / animated
/// via XAML layout instead of by driving a real <c>WindowEx</c>.
/// </summary>
public sealed partial class MainPaneControl : UserControl
{
private Task<Dictionary<string, string?>>? _getAppIdsTask;
private Dictionary<string, string?> _currentApplicationIds = [];
private ShortcutFile? _shortcutFile;
private string _selectedAppName = string.Empty;
/// <summary>
/// Raised whenever the user selects a different app in the nav list.
/// The boolean payload indicates whether the newly-selected app
/// exposes a <c>&lt;TASKBAR1-9&gt;</c> section, i.e. whether the
/// overlay should reveal the taskbar number pseudo-window.
/// </summary>
public event EventHandler<bool>? SelectedAppTaskbarVisibilityChanged;
/// <summary>
/// Raised when the user clicks the close button. The overlay window
/// handles this the same way as pressing Escape.
/// </summary>
public event EventHandler? CloseRequested;
public MainPaneControl()
{
this.InitializeComponent();
Program.CopyAndIndexGenerationThread.Join();
this.TitleTextBlock.Text = ResourceLoaderInstance.ResourceLoader.GetString("Title");
this.Unloaded += OnUnloaded;
}
public async Task Open()
{
// Same background work the original MainWindow ran in its
// constructor: wait for the index-generation thread to finish
// and then enumerate the apps to populate the nav list.
_getAppIdsTask = Task.Run(async () =>
{
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds(Program.ForegroundWindowHandle);
return _currentApplicationIds;
});
await InitializeNavItemsAsync();
}
public void Hide()
{
if (this.ContentFrame.Content is ShortcutsPage currentPage)
{
currentPage.Rows.Clear();
}
this.ContentFrame.Navigate(typeof(BlankPage));
if (this.ContentFrame.BackStack != null)
{
this.ContentFrame.BackStack.Clear();
}
this.ContentFrame.Content = null;
foreach (var item in this.WindowSelector.MenuItems.OfType<NavigationViewItem>())
{
if (item.Icon is ImageIcon imageIcon)
{
imageIcon.Source = null;
}
if (item.Icon is PathIcon pathIcon)
{
pathIcon.Data = null;
}
item.Icon = null;
item.Content = null;
}
this.WindowSelector.MenuItems.Clear();
_shortcutFile = null;
_currentApplicationIds.Clear();
_getAppIdsTask?.Dispose();
_getAppIdsTask = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
internal string SelectedAppName => _selectedAppName;
private void OnUnloaded(object sender, RoutedEventArgs e)
{
_getAppIdsTask?.Dispose();
}
/// <summary>
/// Awaits the background app-id enumeration and populates the nav
/// list. Safe to call repeatedly: the work only runs once.
/// </summary>
private async Task InitializeNavItemsAsync()
{
if (_getAppIdsTask == null)
{
return;
}
try
{
_currentApplicationIds = await _getAppIdsTask.ConfigureAwait(true);
this.SetNavItems();
}
catch (Exception ex)
{
Logger.LogError("Failed to initialize navigation items.", ex);
// Surface the failure so the overlay can close itself,
// mirroring the original MainWindow.InitializeNavItemsAsync
// behavior.
this.DispatcherQueue.TryEnqueue(() => InitializationFailed?.Invoke(this, EventArgs.Empty));
}
}
/// <summary>
/// Raised when the background app-id enumeration fails. The overlay
/// listens to this so it can close itself (same behavior the
/// original <c>MainWindow.InitializeNavItemsAsync</c> hand-coded).
/// </summary>
public event EventHandler? InitializationFailed;
private IconElement BuildNavIcon(string? executablePath)
{
// FIX: Use placeholder initially to reduce upfront memory
// Icons can be loaded on SelectionChanged if needed
return new FontIcon { Glyph = "\uEB91" };
}
private void SetNavItems()
{
this.WindowSelector.MenuItems.Clear();
if (this.WindowSelector.MenuItems.Count != 0)
{
return;
}
string defaultShellName = ManifestInterpreter.GetCachedIndexYamlFile().DefaultShellName;
foreach (var (item, executablePath) in this._currentApplicationIds)
{
if (item == defaultShellName)
{
var pathData = (string)Application.Current.Resources["WindowsLogoPathData"];
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = "Windows", Icon = CreatePathIcon(pathData) });
}
else if (item == "Microsoft.PowerToys")
{
var pathData = (string)Application.Current.Resources["PowerToysLogoPathData"];
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = CreatePathIcon(pathData) });
}
else
{
try
{
IconElement icon = BuildNavIcon(executablePath);
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = icon });
}
catch (IOException ex)
{
Logger.LogError($"Failed to build nav item for application '{item}' (executable '{executablePath}').", ex);
}
}
}
if (this.WindowSelector.MenuItems.Count > 0)
{
this.WindowSelector.SelectedItem = this.WindowSelector.MenuItems[0];
}
}
private static PathIcon CreatePathIcon(string pathData)
{
var geometry = (Geometry)XamlBindingHelper.ConvertValue(typeof(Geometry), pathData);
return new PathIcon
{
Data = geometry,
Width = 20,
Height = 20,
};
}
private void WindowSelector_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItem is not NavigationViewItem selectedItem)
{
return;
}
if (selectedItem.Icon is FontIcon && _currentApplicationIds.TryGetValue(selectedItem.Name, out string? exePath))
{
BitmapImage? bitmap = IconHelper.TryGetExecutableIcon(exePath);
if (bitmap is not null)
{
selectedItem.Icon = new ImageIcon { Source = bitmap };
}
}
this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
bool exposesTaskbarSection = false;
if (this._shortcutFile is ShortcutFile file)
{
exposesTaskbarSection = file.Shortcuts is not null &&
file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true);
if (this.ContentFrame.Content is ShortcutsPage currentPage)
{
currentPage.ClearData();
}
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
}
SelectedAppTaskbarVisibilityChanged?.Invoke(this, exposesTaskbarSection);
}
private void Settings_Tapped(object sender, TappedRoutedEventArgs e)
{
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ShortcutGuide);
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
CloseRequested?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -34,6 +34,7 @@
Visibility="{x:Bind Shortcut.Description, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
</StackPanel>
<ItemsControl
x:Name="OuterItemsControl"
Grid.Column="1"
VerticalAlignment="Center"
ItemsSource="{x:Bind Shortcut.Shortcut, Mode=OneWay}">
@@ -44,7 +45,7 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:ShortcutDescription">
<ItemsControl ItemsSource="{Binding Converter={StaticResource ShortcutDescriptionToKeysConverter}}">
<ItemsControl x:Name="InnerItemsControl" ItemsSource="{Binding Converter={StaticResource ShortcutDescriptionToKeysConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />

View File

@@ -2,9 +2,11 @@
// 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.Linq;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using ShortcutGuide.Helpers;
using ShortcutGuide.Models;
@@ -27,6 +29,81 @@ namespace ShortcutGuide.Controls
public ShortcutItemView()
{
this.InitializeComponent();
this.Unloaded += OnUnloaded;
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
ClearNestedItemsControls(this.OuterItemsControl);
this.OuterItemsControl.ItemsSource = null;
if (this.ContextFlyout is MenuFlyout flyout)
{
flyout.Opening -= PinFlyout_Opening!;
}
this.ContextFlyout = null;
this.ClearValue(ShortcutProperty);
}
/// <summary>
/// Recursively clears ItemsSource for all nested ItemsControl instances
/// to break circular references and release converter-generated collections
/// </summary>
private void ClearNestedItemsControls(ItemsControl parentControl)
{
if (parentControl == null || parentControl.Items == null)
{
return;
}
// Iterate through realized items
for (int i = 0; i < parentControl.Items.Count; i++)
{
// Get the container for this item
var container = parentControl.ContainerFromIndex(i) as FrameworkElement;
if (container == null)
{
continue;
}
// Find any nested ItemsControl in the visual tree
var nestedItemsControls = FindVisualChildren<ItemsControl>(container);
foreach (var nestedControl in nestedItemsControls)
{
ClearNestedItemsControls(nestedControl);
nestedControl.ItemsSource = null;
}
}
}
/// <summary>
/// Finds all visual children of a specific type in the visual tree
/// </summary>
private static System.Collections.Generic.IEnumerable<T> FindVisualChildren<T>(DependencyObject parent)
where T : DependencyObject
{
if (parent == null)
{
yield break;
}
int childCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T typedChild)
{
yield return typedChild;
}
foreach (var descendant in FindVisualChildren<T>(child))
{
yield return descendant;
}
}
}
private void PinFlyout_Opening(object sender, object e)

View File

@@ -3,27 +3,89 @@
x:Class="ShortcutGuide.Controls.TaskbarIndicator"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:backdrops="using:ShortcutGuide.Backdrops"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:ShortcutGuide"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid HorizontalAlignment="Stretch">
<!--
Each indicator is a standalone tooltip-style element with a body
(rounded rect) and a downward-pointing triangle tail, similar to
TeachingTip's visual. The tail points toward the corresponding
taskbar button below.
-->
<Grid
HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!--
Windows 11 system flyout animation: slide up + fade from the
taskbar edge. Same timing as Action Center / Widgets pane
(~367ms entrance, ~200ms exit). No scale.
-->
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
From="0"
To="1.0"
Duration="0:0:0.367" />
<animations:TranslationAnimation
EasingMode="EaseOut"
EasingType="Cubic"
From="0,12,0"
To="0,0,0"
Duration="0:0:0.367" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:OpacityAnimation
From="1.0"
To="0"
Duration="0:0:0.200" />
<animations:TranslationAnimation
EasingMode="EaseIn"
EasingType="Cubic"
From="0,0,0"
To="0,12,0"
Duration="0:0:0.200" />
</animations:Implicit.HideAnimations>
<!-- Body -->
<Border
x:Name="IndicatorRectangle"
Width="36"
Height="36"
MinWidth="40"
MinHeight="40"
Padding="8"
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource ControlCornerRadius}">
CornerRadius="8"
Translation="0,0,32">
<Border.Shadow>
<ThemeShadow />
</Border.Shadow>
<TextBlock
x:Name="IndicatorText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
FontSize="16"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind Label, Mode=OneWay}"
TextAlignment="Center" />
</Border>
<!-- Triangle tail pointing down — only the two diagonal edges are
stroked (no top line), so it seamlessly blends with the body
border above. The negative margin overlaps the body's bottom
border by 1px to hide the seam. -->
<Path
Grid.Row="1"
Margin="0,-1,0,0"
HorizontalAlignment="Center"
Data="M 0,0 L 6,6 L 12,0"
Fill="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
Stroke="{ThemeResource SurfaceStrokeColorDefaultBrush}"
StrokeThickness="1" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="ShortcutGuide.Controls.TaskbarPaneControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<!--
No shared background bar. Each indicator is a standalone tooltip
positioned directly above its corresponding taskbar button.
-->
<Canvas x:Name="KeyHolder" />
</UserControl>

View File

@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation
// 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.Globalization;
using ManagedCommon;
using Microsoft.UI.Xaml.Controls;
using ShortcutGuide.Helpers;
using static ShortcutGuide.NativeMethods;
namespace ShortcutGuide.Controls
{
/// <summary>
/// The taskbar number-indicator pseudo-window that used to be
/// <c>TaskbarWindow</c>. Now a regular <see cref="UserControl"/> hosted
/// inside <see cref="OverlayWindow"/>; the overlay applies the values
/// returned from <see cref="UpdateTasklistButtons"/> to position the
/// control inside its <see cref="Canvas"/>.
/// </summary>
public sealed partial class TaskbarPaneControl : UserControl
{
/// <summary>
/// The window-relative layout the overlay should apply to this control.
/// All values are in DIPs.
/// </summary>
public readonly record struct TaskbarPaneLayout(double Left, double Top, double Width, double Height);
public TaskbarPaneControl()
{
this.InitializeComponent();
}
/// <summary>
/// Rebuilds the indicator children from the current taskbar buttons
/// and returns the desired position/size the overlay should apply.
/// Returns <see langword="null"/> when there are no taskbar buttons
/// (caller should hide the control).
/// </summary>
/// <param name="overlayPhysicalOriginX">The overlay window's physical left in screen coordinates.</param>
/// <param name="overlayPhysicalOriginY">The overlay window's physical top in screen coordinates.</param>
/// <param name="dpi">DPI scale factor of the host overlay window.</param>
/// <param name="workAreaBottomPhysical">Bottom of the work area in physical pixels.</param>
public TaskbarPaneLayout? UpdateTasklistButtons(int overlayPhysicalOriginX, int overlayPhysicalOriginY, float dpi, double workAreaBottomPhysical)
{
TasklistButton[] buttons = [];
try
{
buttons = TasklistPositions.GetButtons();
}
catch (Exception ex)
{
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
}
if (buttons.Length == 0)
{
this.KeyHolder.Children.Clear();
return null;
}
// Each indicator is a standalone tooltip (~36px body + ~6px triangle
// tail). Position the pane so that the bottom of the triangle sits
// 8px above the bottom of the work area (taskbar edge).
const double IndicatorBodyDip = 40;
const double TriangleTailDip = 6;
const double BottomMarginDip = 8;
double indicatorTotalHeightDip = IndicatorBodyDip + TriangleTailDip;
double paneOriginPhysicalY = workAreaBottomPhysical - ((indicatorTotalHeightDip + BottomMarginDip) * dpi);
this.KeyHolder.Children.Clear();
double leftmostPhysicalX = buttons[0].X;
double rightmostPhysicalX = buttons[0].X + buttons[0].Width;
foreach (TasklistButton b in buttons)
{
TaskbarIndicator indicator = new()
{
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
};
this.KeyHolder.Children.Add(indicator);
// Center each indicator over its button
double buttonCenterPhysical = b.X + (b.Width / 2.0);
double indicatorLeftDip = ((buttonCenterPhysical - leftmostPhysicalX) / dpi) - (IndicatorBodyDip / 2.0);
Canvas.SetLeft(indicator, indicatorLeftDip);
Canvas.SetTop(indicator, 0);
rightmostPhysicalX = Math.Max(rightmostPhysicalX, b.X + b.Width);
}
double paneLeftDip = (leftmostPhysicalX - overlayPhysicalOriginX) / dpi;
double paneTopDip = (paneOriginPhysicalY - overlayPhysicalOriginY) / dpi;
double paneWidthDip = (rightmostPhysicalX - leftmostPhysicalX) / dpi;
return new TaskbarPaneLayout(paneLeftDip, paneTopDip, paneWidthDip, indicatorTotalHeightDip);
}
}
}

View File

@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="ShortcutGuide.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:ShortcutGuide.Models"
xmlns:winuiex="using:WinUIEx"
Width="586"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
mc:Ignorable="d">
<winuiex:WindowEx.SystemBackdrop>
<DesktopAcrylicBackdrop />
</winuiex:WindowEx.SystemBackdrop>
<Page x:Name="MainPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Uid="TitleBar">
<TitleBar.IconSource>
<ImageIconSource ImageSource="/Assets/ShortcutGuide/ShortcutGuide.ico" />
</TitleBar.IconSource>
</TitleBar>
<NavigationView
x:Name="WindowSelector"
Grid.Row="1"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
SelectionChanged="WindowSelector_SelectionChanged"
Style="{StaticResource RailNavigationViewStyle}">
<NavigationView.MenuItems />
<NavigationView.FooterMenuItems>
<NavigationViewItem
x:Uid="SettingsButton"
Icon="Setting"
SelectsOnInvoked="False"
Tag="Settings"
Tapped="Settings_Tapped" />
</NavigationView.FooterMenuItems>
<NavigationView.Content>
<Frame
x:Name="ContentFrame"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1,1,0,0"
CornerRadius="8,0,0,0" />
</NavigationView.Content>
<NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
</NavigationView.Resources>
</NavigationView>
</Grid>
</Page>
</winuiex:WindowEx>

View File

@@ -1,317 +0,0 @@
// Copyright (c) Microsoft Corporation
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Common.UI;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using ShortcutGuide.Helpers;
using ShortcutGuide.Models;
using ShortcutGuide.Pages;
using Windows.Foundation;
using Windows.Graphics;
using Windows.System;
using Windows.UI.WindowManagement;
using WinRT.Interop;
using WinUIEx;
using WinUIEx.Messaging;
namespace ShortcutGuide
{
public sealed partial class MainWindow : WindowEx, IDisposable
{
private readonly Stopwatch _sessionStopwatch = Stopwatch.StartNew();
private readonly Task<Dictionary<string, string?>> _getAppIdsTask;
private Dictionary<string, string?> _currentApplicationIds = [];
private ShortcutFile? _shortcutFile;
private string _selectedAppName = null!;
private string _closeType = "Unknown";
internal long SessionDurationMs => _sessionStopwatch.ElapsedMilliseconds;
internal string CloseType => _closeType;
private bool _setPosition;
public MainWindow()
{
this.InitializeComponent();
_getAppIdsTask = Task.Run(() =>
{
Program.CopyAndIndexGenerationThread.Join();
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds(Program.ForegroundWindowHandle);
return _currentApplicationIds;
});
Title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
ExtendsContentIntoTitleBar = true;
#if !DEBUG
this.SetIsAlwaysOnTop(true);
this.SetIsShownInSwitchers(false);
#endif
WindowMessageMonitor msgMonitor = new(this);
msgMonitor.WindowMessageReceived += (_, e) =>
{
const int WM_NCLBUTTONDBLCLK = 0x00A3;
if (e.Message.MessageId == WM_NCLBUTTONDBLCLK)
{
// Disable double click on title bar to maximize window
e.Result = 0;
e.Handled = true;
}
};
Activated += Window_Activated;
Content.KeyUp += (_, e) =>
{
if (e.Key == VirtualKey.Escape)
{
_closeType = "Escape";
Close();
}
};
switch (App.ShortcutGuideProperties.Theme.Value)
{
case "dark":
((FrameworkElement)Content).RequestedTheme = ElementTheme.Dark;
this.MainPage.RequestedTheme = ElementTheme.Dark;
break;
case "light":
((FrameworkElement)Content).RequestedTheme = ElementTheme.Light;
this.MainPage.RequestedTheme = ElementTheme.Light;
break;
case "system":
// Ignore, as the theme will be set by the system.
break;
default:
Logger.LogError("Invalid theme value in settings: " + App.ShortcutGuideProperties.Theme.Value);
break;
}
}
protected override void OnStateChanged(WindowState state)
{
if (state == WindowState.Maximized)
{
this.SetWindowPosition();
}
}
protected override void OnPositionChanged(PointInt32 position)
{
this.SetWindowPosition();
}
private void Window_Activated(object sender, WindowActivatedEventArgs e)
{
if (e.WindowActivationState == WindowActivationState.Deactivated && !this._taskBarWindowActivated)
{
#if !DEBUG
_closeType = "Deactivated";
Close();
#endif
}
if (this._taskBarWindowActivated)
{
this._taskBarWindowActivated = false;
this.BringToFront();
}
// The code below sets the position of the window to the center of the monitor, but only if it hasn't been set before.
if (!this._setPosition)
{
this.SetWindowPosition();
this._setPosition = true;
AppWindow.Changed += (_, a) =>
{
if (!a.DidPresenterChange)
{
return;
}
this.SetWindowPosition();
};
}
_ = this.InitializeNavItemsAsync();
}
private async Task InitializeNavItemsAsync()
{
try
{
_currentApplicationIds = await _getAppIdsTask.ConfigureAwait(true);
this.SetNavItems();
}
catch (Exception ex)
{
Logger.LogError("Failed to initialize navigation items.", ex);
_closeType = "InitializationFailed";
this.DispatcherQueue.TryEnqueue(() => this.Close());
}
}
private void SetNavItems()
{
// Populate the window selector with the current application IDs if it is empty.
// TO DO: Check if Settings button is considered an item too.
if (this.WindowSelector.MenuItems.Count == 0)
{
string defaultShellName = ManifestInterpreter.GetCachedIndexYamlFile().DefaultShellName;
foreach (var (item, executablePath) in this._currentApplicationIds)
{
if (item == defaultShellName)
{
var pathData = (string)Application.Current.Resources["WindowsLogoPathData"];
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = "Windows", Icon = CreatePathIcon(pathData) });
}
else if (item == "Microsoft.PowerToys")
{
var pathData = (string)Application.Current.Resources["PowerToysLogoPathData"];
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = CreatePathIcon(pathData) });
}
else
{
try
{
IconElement icon = BuildNavIcon(executablePath);
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = icon });
}
catch (IOException ex)
{
Logger.LogError($"Failed to build nav item for application '{item}' (executable '{executablePath}').", ex);
}
}
}
if (this.WindowSelector.MenuItems.Count > 0)
{
this.WindowSelector.SelectedItem = this.WindowSelector.MenuItems[0];
}
}
}
private static IconElement BuildNavIcon(string? executablePath)
{
BitmapImage? bitmap = IconHelper.TryGetExecutableIcon(executablePath);
if (bitmap is not null)
{
return new ImageIcon { Source = bitmap };
}
return new FontIcon { Glyph = "\uEB91" };
}
private static PathIcon CreatePathIcon(string pathData)
{
var geometry = (Geometry)XamlBindingHelper.ConvertValue(typeof(Geometry), pathData);
return new PathIcon
{
Data = geometry,
Width = 20,
Height = 20,
};
}
private bool _hasMovedToRightMonitor;
private void SetWindowPosition()
{
if (!this._hasMovedToRightMonitor)
{
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
this._hasMovedToRightMonitor = true;
}
var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
var taskbarWindow = App.TaskBarWindow.AppWindow;
bool taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
bool taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
double newHeight = monitorRect.Height / dpi;
if (taskbarOnLeft || taskbarOnRight)
{
newHeight -= taskbarWindow.Size.Height;
}
MaxHeight = newHeight;
MinHeight = newHeight;
Height = newHeight;
int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
: (int)monitorRect.X;
this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
}
/// <summary>
/// Tracks whether the taskbar window was activated. So that the main window does not close.
/// </summary>
private bool _taskBarWindowActivated;
private void WindowSelector_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItem is not NavigationViewItem selectedItem)
{
return;
}
this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
App.TaskBarWindow.Hide();
if (this._shortcutFile is ShortcutFile file)
{
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
{
this._taskBarWindowActivated = true;
App.TaskBarWindow.Activate();
}
// Reposition before navigating so the taskbar window does not clip into the main window.
this.SetWindowPosition();
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
}
}
private void Settings_Tapped(object sender, TappedRoutedEventArgs e)
{
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ShortcutGuide);
}
public void Dispose()
{
_getAppIdsTask.Dispose();
}
}
}

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="ShortcutGuide.OverlayWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:ShortcutGuide.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winuiex="using:WinUIEx"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False"
mc:Ignorable="d">
<!--
Full-monitor transparent host. The system backdrop is set in code-
behind to TransparentTintBackdrop so the area outside the pseudo-
windows is fully see-through; per-card acrylic is supplied inside
each UserControl via SystemBackdropElement + AlwaysActiveDesktopAcrylicBackdrop.
-->
<!--
OverlayRoot must have a Background brush so it is hit-test opaque —
otherwise clicks on the transparent margin fall through to whatever
is underneath and the click-outside-to-close UX breaks. A fully
transparent brush is enough; the visual transparency comes from the
system backdrop above.
-->
<Grid
x:Name="OverlayRoot"
Background="Transparent"
PointerPressed="OverlayRoot_PointerPressed">
<controls:MainPaneControl
x:Name="MainPane"
Width="586"
Margin="16"
Horizo ntalAlignment="Left"
VerticalAlignment="Stretch"
Visibility="Collapsed" />
<!--
TaskbarPane sits in an absolute layer (Canvas) because it must
align pixel-perfectly with the underlying taskbar buttons. The
overlay positions it via Canvas.SetLeft/Top once UpdateLayout
returns the desired rect.
-->
<Canvas x:Name="TaskbarLayer" IsHitTestVisible="True">
<controls:TaskbarPaneControl x:Name="TaskbarPane" Visibility="Collapsed" />
</Canvas>
</Grid>
</winuiex:WindowEx>

View File

@@ -0,0 +1,540 @@
// Copyright (c) Microsoft Corporation
// 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.Diagnostics;
using System.Runtime.InteropServices;
using Common.UI;
using CommunityToolkit.WinUI.Animations;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using ShortcutGuide.Controls;
using ShortcutGuide.Helpers;
using Windows.Foundation;
using Windows.System;
using WinRT.Interop;
using WinUIEx;
using WinUIEx.Messaging;
namespace ShortcutGuide
{
/// <summary>
/// Single transparent host that replaces both the previous
/// <c>MainWindow</c> and <c>TaskbarWindow</c>. It covers the work area
/// of the cursor's monitor and renders the two surfaces as pseudo-windows
/// (<see cref="MainPaneControl"/> + <see cref="TaskbarPaneControl"/>)
/// inside its XAML tree, so they can be coordinated by a single
/// dispatcher, activate together, and (later) share an animation
/// timeline.
/// </summary>
public sealed partial class OverlayWindow : WindowEx
{
// DWM attributes used to disable the Win11 1-pixel system border and
// forced corner rounding. Without these the OS draws a faint stroke
// (and a small drop-shadow) around the transparent overlay, which
// looks like a phantom rectangle on top of the desktop.
private const uint DwmwaColorNone = 0xFFFFFFFE;
private const int DwmwaWindowCornerPreference = 33;
private const int DwmwaBorderColor = 34;
private const int DwmwcpDoNotRound = 1;
private readonly Stopwatch _sessionStopwatch = Stopwatch.StartNew();
private string _closeType = "Unknown";
private bool _isClosing;
// Set true around AppWindow.MoveAndResize() so the WndProc swallows
// WM_DPICHANGED. Without this, WinUIEx re-scales the rect we just
// wrote by (newDpi / oldDpi), producing a 1.5x/0.66x window on
// cross-monitor moves between mixed-DPI displays.
private bool _suppressDpiChange;
internal long SessionDurationMs => _sessionStopwatch.ElapsedMilliseconds;
internal string CloseType => _closeType;
public MainPaneControl MainPaneControl => this.MainPane;
internal TaskbarPaneControl TaskbarPaneControl => this.TaskbarPane;
public OverlayWindow()
{
this.InitializeComponent();
// TransparentTintBackdrop cannot be referenced from XAML in the
// current Windows App SDK schema; set it here instead.
this.SystemBackdrop = new TransparentTintBackdrop();
this.Title = ResourceLoaderInstance.ResourceLoader.GetString("Title");
this.ExtendsContentIntoTitleBar = true;
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
// Install the message hook BEFORE the first MoveAndResize so the
// WM_DPICHANGED suppression is in place from the very first
// cross-monitor move. Otherwise the constructor's initial
// RepositionToCursorMonitor() lets the default WndProc auto-
// resize the window by (newDpi/oldDpi) and we get a 1.5×
// overlay on the laptop screen. Also disable
// WM_NCLBUTTONDBLCLK so the OS doesn't try to maximize when
// the user double-clicks anywhere in the (collapsed) caption
// area. Matches the original MainWindow behavior.
WindowMessageMonitor msgMonitor = new(this);
msgMonitor.WindowMessageReceived += (_, e) =>
{
const int WM_NCLBUTTONDBLCLK = 0x00A3;
const int WM_DPICHANGED = 0x02E0;
if (e.Message.MessageId == WM_NCLBUTTONDBLCLK)
{
e.Result = 0;
e.Handled = true;
return;
}
if (e.Message.MessageId == WM_DPICHANGED && _suppressDpiChange)
{
// We've already written the correctly-scaled physical
// rect for the target monitor — let WinUI update its
// RasterizationScale, but DO NOT let the default
// WndProc resize the window to its suggested rect
// (which is the OLD size scaled by newDpi/oldDpi and
// is what produced the doubled overlay).
e.Result = 0;
e.Handled = true;
}
};
StripNativeChrome();
// Pre-size and position BEFORE Activate so the first frame the
// user sees is already at the correct work-area size on the
// cursor's monitor. Doing this in OnActivated produces a single
// wrong-sized flash that "snaps" to the right size when focus
// changes (because a later layout pass picks up the correct
// AppWindow.Size).
RepositionToCursorMonitor();
ApplyMainPaneAlignment();
#if !DEBUG
this.SetIsAlwaysOnTop(true);
this.SetIsShownInSwitchers(false);
#endif
this.Activated += OnActivated;
// Esc closes the overlay regardless of which pseudo-window has
// keyboard focus (handled at the Window.Content root because the
// event bubbles up from whichever inner element has focus).
if (this.Content is UIElement contentRoot)
{
contentRoot.KeyUp += OnContentKeyUp;
}
ApplyThemeFromSettings();
// Reposition when the window's presenter changes (e.g. from a
// PresenterKind switch). Matches the original MainWindow behavior.
this.AppWindow.Changed += (_, args) =>
{
if (!args.DidPresenterChange)
{
return;
}
RepositionToCursorMonitor();
};
this.MainPane.SelectedAppTaskbarVisibilityChanged += OnMainPaneTaskbarVisibilityChanged;
this.MainPane.InitializationFailed += OnMainPaneInitializationFailed;
this.MainPane.CloseRequested += (_, _) =>
{
_closeType = "CloseButton";
CloseAnimated();
};
// Reveal the main pane after the window has loaded so the
// Implicit.ShowAnimations play on first appearance.
this.OverlayRoot.Loaded += (_, _) =>
{
this.MainPane.Visibility = Visibility.Visible;
};
}
/// <summary>
/// Strips the native window frame (WS_CAPTION/WS_THICKFRAME), the
/// extended edge styles (WS_EX_WINDOWEDGE/CLIENTEDGE/DLGMODALFRAME)
/// and disables the Win11 DWM 1-px system border + forced corner
/// rounding. Also marks the window as a tool window
/// (WS_EX_TOOLWINDOW) so the OS doesn't draw the default toplevel
/// chrome that produces the faint white outline on transparent
/// surfaces. Idempotent and safe to call again after the window has
/// been moved across monitors — a cross-monitor DPI change can reset
/// some of these attributes.
/// </summary>
private void StripNativeChrome()
{
nint hwnd = WindowNative.GetWindowHandle(this);
HwndExtensions.ToggleWindowStyle(hwnd, false, WindowStyle.TiledWindow);
// Extended styles: drop the 3-D-ish window edges Windows draws for
// ordinary top-level windows and switch the window into the
// "tool window" category. WS_EX_TOOLWINDOW also suppresses the
// small caption Windows would otherwise reserve, which removes
// the last visible 1-px line around a transparent overlay.
int exStyle = NativeMethods.GetWindowLongW(hwnd, NativeMethods.GWL_EXSTYLE);
int newExStyle = (exStyle
& ~NativeMethods.WS_EX_WINDOWEDGE
& ~NativeMethods.WS_EX_CLIENTEDGE
& ~NativeMethods.WS_EX_DLGMODALFRAME)
| NativeMethods.WS_EX_TOOLWINDOW;
if (newExStyle != exStyle)
{
_ = NativeMethods.SetWindowLongW(hwnd, NativeMethods.GWL_EXSTYLE, newExStyle);
}
// SWP_FRAMECHANGED forces DWM to re-evaluate the frame after
// style changes and after DwmExtendFrameIntoClientArea below.
const uint SWP_NOMOVE = 0x0002;
const uint SWP_NOSIZE = 0x0001;
const uint SWP_NOZORDER = 0x0004;
const uint SWP_NOACTIVATE = 0x0010;
const uint SWP_FRAMECHANGED = 0x0020;
uint borderColor = DwmwaColorNone;
_ = DwmSetWindowAttribute(hwnd, DwmwaBorderColor, ref borderColor, sizeof(uint));
int cornerPref = DwmwcpDoNotRound;
_ = DwmSetWindowAttribute(hwnd, DwmwaWindowCornerPreference, ref cornerPref, sizeof(int));
// Disable non-client rendering entirely so the DWM doesn't draw
// ANY frame/border chrome (not even a 1-px line).
int ncrpDisabled = 2; // DWMNCRP_DISABLED
_ = DwmSetWindowAttribute(hwnd, 2 /* DWMWA_NCRENDERING_POLICY */, ref ncrpDisabled, sizeof(int));
// Extend the frame into the entire client area. With a transparent
// backdrop this eliminates the last possible seam between the
// non-client and client regions that the DWM might draw.
var margins = new MARGINS { Left = -1, Right = -1, Top = -1, Bottom = -1 };
_ = DwmExtendFrameIntoClientArea(hwnd, ref margins);
_ = NativeMethods.SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
private void ApplyThemeFromSettings()
{
switch (App.ShortcutGuideProperties.Theme.Value)
{
case "dark":
if (this.Content is FrameworkElement darkRoot)
{
darkRoot.RequestedTheme = ElementTheme.Dark;
}
break;
case "light":
if (this.Content is FrameworkElement lightRoot)
{
lightRoot.RequestedTheme = ElementTheme.Light;
}
break;
case "system":
// Default — follow the system theme.
break;
default:
Logger.LogError("Invalid theme value in settings: " + App.ShortcutGuideProperties.Theme.Value);
break;
}
}
private void OnActivated(object sender, WindowActivatedEventArgs e)
{
if (e.WindowActivationState == WindowActivationState.Deactivated)
{
#if !DEBUG
_closeType = "Deactivated";
CloseAnimated();
#endif
return;
}
}
private void OnContentKeyUp(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Escape)
{
_closeType = "Escape";
CloseAnimated();
}
}
/// <summary>
/// Closes the overlay when the user clicks outside the pseudo-windows
/// (i.e. anywhere in the transparent area). Clicks on
/// <see cref="MainPaneControl"/> or <see cref="TaskbarPaneControl"/>
/// are ignored — we detect them by walking the visual tree from
/// <see cref="RoutedEventArgs.OriginalSource"/>.
/// </summary>
private void OverlayRoot_PointerPressed(object sender, PointerRoutedEventArgs e)
{
if (e.OriginalSource is DependencyObject src && IsInsidePseudoWindow(src))
{
return;
}
_closeType = "ClickOutside";
CloseAnimated();
}
private bool IsInsidePseudoWindow(DependencyObject src)
{
DependencyObject? current = src;
while (current is not null)
{
if (ReferenceEquals(current, this.MainPane) || ReferenceEquals(current, this.TaskbarPane))
{
return true;
}
current = VisualTreeHelper.GetParent(current);
}
return false;
}
private void OnMainPaneTaskbarVisibilityChanged(object? sender, bool shouldShow)
{
if (!shouldShow)
{
this.TaskbarPane.Visibility = Visibility.Collapsed;
return;
}
UpdateTaskbarPaneLayout();
}
private void OnMainPaneInitializationFailed(object? sender, EventArgs e)
{
_closeType = "InitializationFailed";
this.DispatcherQueue.TryEnqueue(() => this.Close());
}
/// <summary>
/// Triggers hide animations on both pseudo-windows, waits for the
/// longest animation to finish, then closes the window. Multiple
/// calls are coalesced via <see cref="_isClosing"/>.
/// </summary>
public void CloseAnimated()
{
if (_isClosing)
{
return;
}
_isClosing = true;
// Collapse both pseudo-windows so their Implicit.HideAnimations play
this.MainPane.Visibility = Visibility.Collapsed;
this.TaskbarPane.Visibility = Visibility.Collapsed;
var timer = this.DispatcherQueue.CreateTimer();
timer.Interval = TimeSpan.FromMilliseconds(217);
timer.IsRepeating = false;
timer.Tick += (_, _) =>
{
timer.Stop();
_isClosing = false;
MainPane.Hide();
this.AppWindow.Hide();
};
timer.Start();
}
/// <summary>
/// Recomputes the taskbar pane's indicator children and applies the
/// resulting layout to the Canvas-positioned pseudo-window.
/// </summary>
public void UpdateTaskbarPaneLayout()
{
var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect workArea = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
var layout = this.TaskbarPane.UpdateTasklistButtons(
overlayPhysicalOriginX: this.AppWindow.Position.X,
overlayPhysicalOriginY: this.AppWindow.Position.Y,
dpi: dpi,
workAreaBottomPhysical: workArea.Bottom);
if (layout is null)
{
this.TaskbarPane.Visibility = Visibility.Collapsed;
return;
}
this.TaskbarPane.Width = layout.Value.Width;
this.TaskbarPane.Height = layout.Value.Height;
Canvas.SetLeft(this.TaskbarPane, layout.Value.Left);
Canvas.SetTop(this.TaskbarPane, layout.Value.Top);
this.TaskbarPane.Visibility = Visibility.Visible;
}
/// <summary>
/// Sizes and positions the overlay to cover the work area of the
/// monitor that currently contains the cursor. Looks the monitor up
/// directly from the cursor position so the work-area rect doesn't
/// depend on a previous asynchronous <see cref="AppWindow.Move"/>
/// having actually committed yet.
/// </summary>
private void RepositionToCursorMonitor()
{
if (!NativeMethods.GetCursorPos(out NativeMethods.POINT cursor))
{
return;
}
IntPtr hmon = NativeMethods.MonitorFromPoint(
cursor,
(int)NativeMethods.MonitorFromWindowDwFlags.MONITOR_DEFAULTTONEAREST);
if (hmon == IntPtr.Zero)
{
return;
}
var mi = new NativeMethods.MONITORINFO
{
CbSize = (uint)Marshal.SizeOf<NativeMethods.MONITORINFO>(),
};
if (!NativeMethods.GetMonitorInfoW(hmon, ref mi))
{
return;
}
// rcWork is in physical pixels (virtual-screen coordinates).
// AppWindow.MoveAndResize takes physical pixels too — no DPI math
// required. Using AppWindow directly avoids the WinUIEx
// WindowEx.MoveAndResize extension whose x/y are physical but
// whose w/h are DIPs (and get DPI-scaled internally).
var rect = new Windows.Graphics.RectInt32(
mi.RcWork.Left,
mi.RcWork.Top,
mi.RcWork.Right - mi.RcWork.Left,
mi.RcWork.Bottom - mi.RcWork.Top);
// Cross-monitor moves trigger WM_DPICHANGED which causes WinUIEx
// to auto-rescale the window by (newDpi/oldDpi) ON TOP of the
// physical rect we just provided — making the overlay 1.5×
// (or 0.66×) the work area on mixed-DPI multi-monitor setups.
// CmdPal's MoveAndResizeDpiAware pattern: zero out MinWidth/
// MinHeight (WinUIEx uses current DPI to recompute them in
// physical px), set _suppressDpiChange so the WndProc swallows
// WM_DPICHANGED, then MoveAndResize.
var origMinWidth = this.MinWidth;
var origMinHeight = this.MinHeight;
_suppressDpiChange = true;
try
{
this.MinWidth = 0;
this.MinHeight = 0;
this.AppWindow.MoveAndResize(rect);
}
finally
{
this.MinWidth = origMinWidth;
this.MinHeight = origMinHeight;
_suppressDpiChange = false;
}
// Cross-monitor moves can trigger WM_DPICHANGED, and Windows may
// reset some of our DWM attributes (border color, corner pref)
// during that transition. Re-apply them defensively so the
// overlay never reveals an OS-drawn 1-px stroke or rounded
// shadow.
StripNativeChrome();
// The taskbar pane is anchored against the bottom of the work area,
// so any move/resize needs a fresh layout pass.
if (this.TaskbarPane.Visibility == Visibility.Visible)
{
UpdateTaskbarPaneLayout();
}
}
/// <summary>
/// Applies the left/right alignment from the user setting to the
/// main pseudo-window.
/// </summary>
private void ApplyMainPaneAlignment()
{
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
bool isRight = windowPosition == ShortcutGuideWindowPosition.Right;
this.MainPane.HorizontalAlignment = isRight
? HorizontalAlignment.Right
: HorizontalAlignment.Left;
// Slide direction matches the pane's edge: left-aligned slides
// from the left, right-aligned slides from the right — same as
// Windows 11 Widgets (left) and Action Center (right).
string slideFrom = isRight ? "20,0,0" : "-20,0,0";
var showAnimations = new ImplicitAnimationSet();
showAnimations.Add(new OpacityAnimation { From = 0, To = 1.0, Duration = TimeSpan.FromMilliseconds(367) });
showAnimations.Add(new TranslationAnimation
{
From = slideFrom,
To = "0,0,0",
Duration = TimeSpan.FromMilliseconds(367),
EasingType = EasingType.Cubic,
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
});
Implicit.SetShowAnimations(this.MainPane, showAnimations);
var hideAnimations = new ImplicitAnimationSet();
hideAnimations.Add(new OpacityAnimation { From = 1.0, To = 0, Duration = TimeSpan.FromMilliseconds(200) });
hideAnimations.Add(new TranslationAnimation
{
From = "0,0,0",
To = slideFrom,
Duration = TimeSpan.FromMilliseconds(200),
EasingType = EasingType.Cubic,
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
});
Implicit.SetHideAnimations(this.MainPane, hideAnimations);
}
[LibraryImport("dwmapi.dll")]
private static partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, ref uint pvAttribute, int cbAttribute);
[LibraryImport("dwmapi.dll")]
private static partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, ref int pvAttribute, int cbAttribute);
[LibraryImport("dwmapi.dll")]
private static partial int DwmExtendFrameIntoClientArea(nint hwnd, ref MARGINS pMarInset);
[StructLayout(LayoutKind.Sequential)]
private struct MARGINS
{
public int Left;
public int Right;
public int Top;
public int Bottom;
}
}
}

View File

@@ -44,7 +44,9 @@
</Page.Resources>
<ScrollViewer>
<ItemsRepeater
x:Name="MainItemsRepeater"
Margin="0,0,0,24"
ElementClearing="MainItemsRepeater_ElementClearing"
ItemTemplate="{StaticResource RowTemplateSelector}"
ItemsSource="{x:Bind Rows, Mode=OneWay}" />
</ScrollViewer>

View File

@@ -6,34 +6,50 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using ShortcutGuide.Controls;
using ShortcutGuide.Helpers;
using ShortcutGuide.Models;
using ShortcutGuide.ViewModels;
namespace ShortcutGuide.Pages
{
/// <summary>
/// Displays every category of shortcuts for the selected application as a single,
/// flat virtualized list (Pinned, Recommended, each category, Taskbar).
/// The list is flat — header / subtitle / shortcut / empty-placeholder rows share one
/// ItemsRepeater — so virtualization realizes only rows in the viewport instead of
/// every row of every realized section.
/// </summary>
public sealed partial class ShortcutsPage : Page
{
private const string TaskbarSectionMarker = "<TASKBAR1-9>";
private ShortcutFile? _shortcutFile;
private string _appName = string.Empty;
private bool _isEventSubscribed;
public ObservableCollection<ShortcutListItem> Rows { get; } = new();
public ShortcutsPage()
{
this.InitializeComponent();
this.Unloaded += (_, _) => PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
this.Unloaded += (_, _) =>
{
UnsubscribeFromEvents();
ClearData();
ForceItemsRepeaterCleanup();
};
}
private void MainItemsRepeater_ElementClearing(ItemsRepeater sender, ItemsRepeaterElementClearingEventArgs args)
{
// Aggressively clean up elements as they're being cleared
if (args.Element is FrameworkElement element)
{
// Clear DataContext to break binding references
element.DataContext = null;
if (element is ShortcutItemView shortcutView)
{
shortcutView.ClearValue(ShortcutItemView.ShortcutProperty);
}
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -45,13 +61,73 @@ namespace ShortcutGuide.Pages
this.RebuildRows();
}
PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
UnsubscribeFromEvents();
PinnedShortcutsHelper.PinnedShortcutsChanged += this.OnPinnedShortcutsChanged;
_isEventSubscribed = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
UnsubscribeFromEvents();
ClearData();
ForceItemsRepeaterCleanup();
}
public void ClearData()
{
// Clear the collection to trigger ElementClearing for all items
this.Rows.Clear();
_shortcutFile = null;
_appName = string.Empty;
}
/// <summary>
/// Forces ItemsRepeater to release all cached/recycled elements and clear WinUI's internal template cache.
/// This addresses the ConcurrentDictionary WeakReference cache leak in WinUI.
/// </summary>
private void ForceItemsRepeaterCleanup()
{
if (this.MainItemsRepeater == null)
{
return;
}
try
{
// Get the parent ScrollViewer
if (this.Content is ScrollViewer scrollViewer)
{
// Create a brand new ItemsRepeater
var newRepeater = new ItemsRepeater
{
Name = "MainItemsRepeater",
Margin = new Thickness(0, 0, 0, 24),
ItemTemplate = this.Resources["RowTemplateSelector"] as IElementFactory,
Layout = new StackLayout(),
};
newRepeater.ElementClearing += MainItemsRepeater_ElementClearing;
scrollViewer.Content = newRepeater;
}
GC.Collect(2, GCCollectionMode.Aggressive, true, true);
GC.WaitForPendingFinalizers();
GC.Collect(2, GCCollectionMode.Aggressive, true, true);
}
catch
{
// Fail silently
}
}
private void UnsubscribeFromEvents()
{
if (_isEventSubscribed)
{
PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
_isEventSubscribed = false;
}
}
private void RebuildRows()
@@ -105,8 +181,6 @@ namespace ShortcutGuide.Pages
{
string name = category.SectionName ?? string.Empty;
// Taskbar marker may carry trailing text in the manifest (e.g. "<TASKBAR1-9>Taskbar Shortcuts");
// detect it by prefix and hand it off to the dedicated Taskbar section below.
if (name.StartsWith(TaskbarSectionMarker, StringComparison.Ordinal))
{
taskbarCategory = category;

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="ShortcutGuide.ShortcutGuideXAML.TaskbarWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:ShortcutGuide"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winuiex="using:WinUIEx"
Title="TaskbarWindow"
Width="600"
Height="200"
IsAlwaysOnTop="True"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False"
mc:Ignorable="d">
<Window.SystemBackdrop>
<DesktopAcrylicBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="WindowsLogoColumnWidth" Width="68" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Canvas x:Name="KeyHolder" Grid.Column="1" />
<StackPanel
Margin="8"
VerticalAlignment="Top"
Orientation="Horizontal"
Spacing="8">
<Border
Width="34"
Height="34"
Padding="8"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
BorderThickness="0"
CornerRadius="{StaticResource ControlCornerRadius}">
<Viewbox>
<PathIcon Data="M9 20H0V11H9V20ZM20 20H11V11H20V20ZM9 9H0V0H9V9ZM20 9H11V0H20V9Z" />
</Viewbox>
</Border>
<TextBlock
Margin="0,-3,0,0"
VerticalAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="+" />
</StackPanel>
</Grid>
</winuiex:WindowEx>

View File

@@ -1,83 +0,0 @@
// Copyright (c) Microsoft Corporation
// 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.Globalization;
using ManagedCommon;
using Microsoft.UI.Xaml.Controls;
using ShortcutGuide.Controls;
using ShortcutGuide.Helpers;
using Windows.Foundation;
using WinRT.Interop;
using WinUIEx;
using static ShortcutGuide.NativeMethods;
namespace ShortcutGuide.ShortcutGuideXAML
{
public sealed partial class TaskbarWindow : WindowEx
{
private float DPI => DpiHelper.GetDPIScaleForWindow(WindowNative.GetWindowHandle(this));
private Rect WorkArea => DisplayHelper.GetWorkAreaForDisplayWithWindow(WindowNative.GetWindowHandle(this));
public TaskbarWindow()
{
this.InitializeComponent();
this.UpdateTasklistButtons();
this.Activated += (_, _) => this.UpdateTasklistButtons();
}
public void UpdateTasklistButtons()
{
// This move ensures the window spawns on the same monitor as the main window
AppWindow.MoveInZOrderAtBottom();
AppWindow.Move(App.MainWindow.AppWindow.Position);
TasklistButton[] buttons = [];
try
{
buttons = TasklistPositions.GetButtons();
}
catch (Exception ex)
{
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
}
if (buttons.Length == 0)
{
AppWindow.Hide();
return;
}
float dpi = this.DPI;
double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value;
double windowHeight = 58;
double windowMargin = 8 * dpi;
double windowWidth = windowsLogoColumnWidth;
double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi);
double yPosition = this.WorkArea.Bottom - (windowHeight * dpi);
this.KeyHolder.Children.Clear();
foreach (TasklistButton b in buttons)
{
TaskbarIndicator indicator = new()
{
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
Height = b.Height / dpi,
Width = b.Width / dpi,
};
windowWidth += indicator.Width;
this.KeyHolder.Children.Add(indicator);
double indicatorPos = (b.X - xPosition) / dpi;
Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth);
}
this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight);
AppWindow.MoveInZOrderAtTop();
}
}
}

View File

@@ -120,6 +120,9 @@
<data name="CloseButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Close</value>
</data>
<data name="CloseButton.AutomationProperties.Name" xml:space="preserve">
<value>Close</value>
</data>
<data name="ErrorInAppParsing" xml:space="preserve">
<value>Error displaying the application's shortcuts</value>
<comment>Shortcuts refers to keyboard shortcuts; Application refers to a PC application</comment>

View File

@@ -10,7 +10,6 @@
#include "../interface/powertoy_module_interface.h"
#include "Generated Files/resource.h"
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/EventWaiter.h>
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD /*ul_reason_for_call*/, LPVOID /*lpReserved*/)
{
@@ -37,9 +36,10 @@ public:
}
triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT);
triggerEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](DWORD) {
OnHotkeyEx();
});
if (!triggerEvent)
{
Logger::warn(L"Failed to create {} event. {}", CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, get_last_error_or_default(GetLastError()));
}
InitSettings();
}
@@ -91,6 +91,7 @@ public:
if (!_enabled)
{
_enabled = true;
StartProcess();
}
else
{
@@ -127,6 +128,10 @@ public:
{
CloseHandle(exitEvent);
}
if (triggerEvent)
{
CloseHandle(triggerEvent);
}
delete this;
}
@@ -145,19 +150,12 @@ public:
return;
}
if (IsProcessActive())
if (!IsProcessActive())
{
TerminateProcess(m_hProcess, 0);
return;
StartProcess();
}
if (m_hProcess)
{
CloseHandle(m_hProcess);
m_hProcess = nullptr;
}
StartProcess();
SetEvent(triggerEvent);
}
virtual void send_settings_telemetry() override
@@ -168,6 +166,8 @@ public:
Logger::error("Failed to create a process to send settings telemetry");
}
}
virtual bool keep_track_of_pressed_win_key() override { return true; }
virtual UINT milliseconds_win_key_must_be_pressed() override { return 900; }
private:
std::wstring app_name;
@@ -187,7 +187,6 @@ private:
HANDLE triggerEvent;
HANDLE exitEvent;
EventWaiter triggerEventWaiter;
bool StartProcess(std::wstring args = L"")
{
@@ -196,6 +195,11 @@ private:
ResetEvent(exitEvent);
}
if (triggerEvent)
{
ResetEvent(triggerEvent);
}
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
@@ -310,6 +314,14 @@ private:
m_hotkey.vkCode = VK_OEM_2;
}
}
void WindowsKeyPressBehavior()
{
if (IsProcessActive())
{
TerminateProcess(m_hProcess, 0);
}
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()