Compare commits

...

8 Commits

Author SHA1 Message Date
Niels Laute
e02f68b286 Push 2026-06-01 10:51:23 +02:00
niels9001
ac557e220b Update check-spelling expect.txt
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 15:55:40 +02:00
Niels Laute
d021b0550f ShortcutGuide: apply transparent acrylic chrome to TaskbarWindow + tighten margins
- TaskbarWindow now derives from TransparentWindow, so it shares the
  same acrylic card chrome (rounded border + shadow) as MainWindow.
- Added a vertical slide-up animation tuned to the actual window height
  after the taskbar-button measurement pass.
- Bumped windowHeight and side margin to give the card a 12 DIP inset
  on every edge, matching MainWindow.
- Updated KeyVisual + TaskbarIndicator backgrounds from acrylic-style
  brushes to solid ControlFillColorDefaultBrush since they now sit on
  the acrylic card instead of a plain backdrop.
- Tightened MainWindow chrome margin 24 -> 12 (width 634 -> 610) so the
  visible card sits closer to the screen edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 14:42:53 +02:00
Niels Laute
fa29c5436f ShortcutGuide: restore MainWindow-first init order; make SetWindowPosition tolerate null TaskBarWindow
TaskbarWindow ctor reads App.MainWindow.AppWindow.Position so MainWindow must be created first. Instead of reordering, allow SetWindowPosition to run while TaskBarWindow is still null - the first call in MainWindow ctor just skips the taskbar-overlap adjustment, which is fine since the taskbar window isn't visible yet anyway.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 12:38:46 +02:00
Niels Laute
05691ee4df ShortcutGuide: position + animate before show; inset card 24px from window edges
- Reorder App.OnLaunched so TaskBarWindow exists before MainWindow ctor runs (SetWindowPosition needs it).

- Move SetWindowPosition + animation attach + initial Visibility=Collapsed from Window_Activated into the ctor, so the window doesn't flash at default location before sliding in.

- Animate the whole Card (acrylic chrome + content) as one unit instead of just the inner Grid.

- Add 24px Margin around the Card and bump window Width 586 -> 634 to keep visible content the same size while giving shadow + screen-edge breathing room.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 12:24:30 +02:00
Niels Laute
859dee310d TransparentWindow: don't collapse native title bar; conflicts with WinUI TitleBar control
ExtendsContentIntoTitleBar=true already hides the system caption, so the explicit PreferredHeightOption=Collapsed was redundant and crashed derived windows that host a WinUI TitleBar control (e.g. ShortcutGuide).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 12:04:01 +02:00
Niels Laute
595961b7a2 ShortcutGuide: derive MainWindow from TransparentWindow for shared acrylic chrome
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 11:47:26 +02:00
Niels Laute
bda8300206 [ShortcutGuide] Slide-in/out animation + reusable TransparentWindow controls
Adds the new PowerToys.Common.UI.Controls library additions (TransparentWindow,
TransparentCard, AlwaysActiveDesktopAcrylicBackdrop) introduced for CmdPal's
toast, and applies a slide-in/out animation to the ShortcutGuide main window.

The page slides in from the same edge the window is pinned to (Left/Right
based on the ShortcutGuideWindowPosition setting) when activated, and slides
back out before closing on Escape, deactivation, or init failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 11:36:15 +02:00
22 changed files with 820 additions and 229 deletions

View File

@@ -466,13 +466,12 @@ DWMNCRENDERINGCHANGED
Dwmp
DWMSENDICONICLIVEPREVIEWBITMAP
DWMSENDICONICTHUMBNAIL
DWMWA
DWMWCP
dwmwa
dwmwcp
DWMWINDOWATTRIBUTE
DWMWINDOWMAXIMIZEDCHANGE
DWORDLONG
dworigin
DWRITE
dxgi
Dxva
eab
@@ -998,7 +997,6 @@ luid
lusrmgr
LVDS
LWA
LWIN
LZero
MAGTRANSFORM
makeappx
@@ -1208,7 +1206,6 @@ nonclient
NONCLIENTMETRICSW
NONELEVATED
nonspace
nonstd
NOOWNERZORDER
NOPARENTNOTIFY
NOPREFIX
@@ -2000,7 +1997,6 @@ valuegenerator
VARTYPE
vbcscompiler
vcamp
VCENTER
vcgtq
VCINSTALLDIR
vcp
@@ -2038,7 +2034,6 @@ vorrq
VOS
vpaddlq
vqsubq
VREDRAW
vreinterpretq
VSC
VSCBD

View File

@@ -247,8 +247,6 @@
"WinUI3Apps\\PowerToys.ShortcutGuide.exe",
"WinUI3Apps\\PowerToys.ShortcutGuide.dll",
"WinUI3Apps\\PowerToys.ShortcutGuideModuleInterface.dll",
"WinUI3Apps\\PowerToys.ShortcutGuide.IndexYmlGenerator.dll",
"WinUI3Apps\\PowerToys.ShortcutGuide.IndexYmlGenerator.exe",
"WinUI3Apps\\ShortcutGuide.CPPProject.dll",
"PowerToys.ZoomIt.exe",

View File

@@ -996,10 +996,6 @@
</Project>
</Folder>
<Folder Name="/modules/ShortcutGuide/">
<Project Path="src/modules/ShortcutGuide/ShortcutGuide.IndexYmlGenerator/ShortcutGuide.IndexYmlGenerator.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuide.Ui.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -94,11 +94,9 @@ If this method fails, which it will for newer versions of Windows, it falls back
It then enumerates all the button elements inside the selected while skipping such with a same name (which implies the user does not use combining taskbar buttons) and such that do not start with "Appid:" (which are not actual taskbar buttons related to apps, but others like the widgets or the search button).
### [`ShortcutGuide.IndexYmlGenerator`](/src/modules/ShortcutGuide/ShortcutGuide.IndexYmlGenerator/)
### `ManifestInterpreter.GenerateIndexYmlFile`
This application generates the `index.yml` manifest file.
It is a separate project so that its code can be easier ported to WinGet in the future.
The `index.yml` manifest file is generated in-process by `ManifestInterpreter.GenerateIndexYmlFile` (in `ShortcutGuide.Ui/Helpers/ManifestInterpreter.cs`), called on a background thread from `Program.Main` before the UI loads. It scans the per-user manifest folder and writes the index used by the navigation/lookup code.
### [`ShortcutGuideModuleInterface`](/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj)

View File

@@ -0,0 +1,103 @@
// 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 Microsoft.PowerToys.Common.UI.Controls.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 transient,
/// non-activating surfaces such as toasts or popups created with
/// <c>SW_SHOWNA</c> / <c>WS_EX_TRANSPARENT</c>, where the window is never
/// activated by design.
///
/// 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.
/// </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

@@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />

View File

@@ -0,0 +1,27 @@
// 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 Microsoft.PowerToys.Common.UI.Controls;
/// <summary>
/// A floating "card" surface for transient PowerToys overlays (toasts,
/// banners, indicators). Provides the PowerToys-standard chrome — 1 px
/// border in <c>SurfaceStrokeColorDefaultBrush</c>, 8 px corner radius,
/// a <c>ThemeShadow</c>, and an always-active desktop acrylic backdrop —
/// via a default <see cref="Microsoft.UI.Xaml.Controls.ControlTemplate"/>.
/// </summary>
/// <remarks>
/// Lives inside a <see cref="Window.TransparentWindow"/>. Apps that want a
/// different look supply their own <c>Style TargetType="TransparentCard"</c>
/// in resources — the standard WinUI restyle path.
/// </remarks>
public sealed partial class TransparentCard : ContentControl
{
public TransparentCard()
{
DefaultStyleKey = typeof(TransparentCard);
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:backdrops="using:Microsoft.PowerToys.Common.UI.Controls.Backdrops"
xmlns:local="using:Microsoft.PowerToys.Common.UI.Controls">
<Style BasedOn="{StaticResource DefaultTransparentCardStyle}" TargetType="local:TransparentCard" />
<Style x:Key="DefaultTransparentCardStyle" TargetType="local:TransparentCard">
<Setter Property="BorderBrush" Value="{ThemeResource SurfaceStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TransparentCard">
<Grid
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Translation="0,0,24">
<Grid.Shadow>
<ThemeShadow />
</Grid.Shadow>
<SystemBackdropElement CornerRadius="{TemplateBinding CornerRadius}">
<SystemBackdropElement.SystemBackdrop>
<backdrops:AlwaysActiveDesktopAcrylicBackdrop />
</SystemBackdropElement.SystemBackdrop>
</SystemBackdropElement>
<ContentPresenter
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -4,5 +4,6 @@
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/TransparentCard/TransparentCard.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,289 @@
// 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.Runtime.InteropServices;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
using WinUIEx;
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
/// <summary>
/// Reusable transparent host window for transient overlays
/// (toasts, banners, indicators) that should not steal foreground.
/// </summary>
/// <remarks>
/// <para>The constructor applies all of the boilerplate that PowerToys overlays
/// currently hand-roll:</para>
/// <list type="bullet">
/// <item>Strip the native frame and caption (<c>WS_THICKFRAME</c> etc.).</item>
/// <item>Disable the Win11 1-pixel DWM border and corner rounding.</item>
/// <item>Mark the window as a tool window so it stays out of the taskbar and Alt-Tab.</item>
/// <item>Extend content into the title bar and collapse the title bar.</item>
/// </list>
/// <para>The visible chrome (acrylic + border + corner radius + shadow) lives
/// in a <see cref="TransparentCard"/> that the constructor assigns to
/// <see cref="Microsoft.UI.Xaml.Window.Content"/>. Consumers supply their own
/// UI via <see cref="InnerContent"/> — which is the XAML default-content slot
/// thanks to <see cref="ContentPropertyAttribute"/> — so a derived window can
/// be written as <c>&lt;common:TransparentWindow&gt;&lt;TextBlock/&gt;&lt;/common:TransparentWindow&gt;</c>.</para>
/// <para>Transparency is achieved with a <see cref="TransparentTintBackdrop"/>
/// system backdrop so the area outside the <see cref="TransparentCard"/> is
/// fully see-through. That buffer area is NOT click-through, so consumers
/// should keep it as small as possible (just enough to give the card's
/// shadow + slide animation room to breathe — roughly 24 px on each side).</para>
/// <para><see cref="Show"/> and <see cref="Hide"/> coordinate <c>SW_SHOWNA</c>
/// (no-activate), the <see cref="Microsoft.UI.Xaml.UIElement.Visibility"/>
/// toggle on the card, and a debounced
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> sized from the longest
/// animation in <see cref="HideAnimations"/>. Animations target the card so
/// the entire surface (border, acrylic, shadow, inner content) slides as one.</para>
/// </remarks>
[ContentProperty(Name = nameof(InnerContent))]
public partial class TransparentWindow : WinUIEx.WindowEx
{
private const uint DwmwaColorNone = 0xFFFFFFFE;
private const int DwmwaWindowCornerPreference = 33;
private const int DwmwaBorderColor = 34;
private const int DwmwcpDoNotRound = 1;
private const int GwlExStyle = -20;
private const int WsExToolWindow = 0x00000080;
private const int SwShowNa = 8;
private readonly DispatcherQueueTimer _hideCloseTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly nint _hwnd;
private readonly TransparentCard _card;
private ImplicitAnimationSet _showAnimations;
private ImplicitAnimationSet _hideAnimations;
public TransparentWindow()
{
AppWindow.Hide();
ExtendsContentIntoTitleBar = true;
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
unsafe
{
uint borderColor = DwmwaColorNone;
_ = DwmSetWindowAttribute(_hwnd, DwmwaBorderColor, &borderColor, sizeof(uint));
int cornerPref = DwmwcpDoNotRound;
_ = DwmSetWindowAttribute(_hwnd, DwmwaWindowCornerPreference, &cornerPref, sizeof(int));
}
ApplyExStyleBit(WsExToolWindow, true);
_showAnimations = BuildDefaultShowAnimations();
_hideAnimations = BuildDefaultHideAnimations();
_card = new TransparentCard();
Content = _card;
SystemBackdrop = new TransparentTintBackdrop();
}
/// <summary>
/// Gets the <see cref="TransparentCard"/> that provides the window's
/// visible chrome (acrylic + border + shadow). Consumers can configure
/// its layout (e.g. <c>HorizontalAlignment</c>, <c>VerticalAlignment</c>,
/// <c>MaxWidth</c>, <c>Margin</c>) to position the card inside the
/// window, or apply a custom <c>Style</c> to change its look.
/// </summary>
public TransparentCard Card => _card;
/// <summary>
/// Gets or sets the visual hosted inside the window's
/// <see cref="TransparentCard"/>. This is the XAML default-content slot:
/// child elements declared between the opening and closing
/// <c>TransparentWindow</c> tags in a derived .xaml are routed here.
/// </summary>
public object? InnerContent
{
get => _card.Content;
set => _card.Content = value;
}
/// <summary>
/// Gets or sets the animations played against
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> when <see cref="Show"/>
/// flips it to <see cref="Visibility.Visible"/>. Defaults to a 200 ms
/// fade-in plus a 250 ms slide-up of 24 px.
/// </summary>
public ImplicitAnimationSet ShowAnimations
{
get => _showAnimations;
set => _showAnimations = value ?? new ImplicitAnimationSet();
}
/// <summary>
/// Gets or sets the animations played against
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> when <see cref="Hide"/>
/// flips it to <see cref="Visibility.Collapsed"/>. Defaults to a 180 ms
/// fade-out plus a 180 ms slide-down of 12 px.
/// </summary>
public ImplicitAnimationSet HideAnimations
{
get => _hideAnimations;
set => _hideAnimations = value ?? new ImplicitAnimationSet();
}
/// <summary>
/// Shows the window without activation (<c>SW_SHOWNA</c>) and flips
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> to
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays.
/// Repeated calls reset the content to its hidden pose first so the show
/// animation re-triggers cleanly. Any pending hide is cancelled.
/// </summary>
public void Show()
{
DispatcherQueue.TryEnqueue(
DispatcherQueuePriority.Low,
() =>
{
_hideCloseTimer.Stop();
if (Content is UIElement content)
{
// Re-apply each call so swapping animation collections at
// runtime takes effect on the next show/hide cycle.
Implicit.SetShowAnimations(content, _showAnimations);
Implicit.SetHideAnimations(content, _hideAnimations);
// Reset to the hidden pose so the show animation always
// animates from the configured starting frame.
content.Visibility = Visibility.Collapsed;
}
_ = ShowWindow(_hwnd, SwShowNa);
if (Content is UIElement c2)
{
c2.Visibility = Visibility.Visible;
}
});
}
/// <summary>
/// Flips <see cref="Microsoft.UI.Xaml.Window.Content"/> to
/// <see cref="Visibility.Collapsed"/> so <see cref="HideAnimations"/>
/// plays, then hides the underlying
/// <see cref="Microsoft.UI.Windowing.AppWindow"/> once the longest
/// animation in <see cref="HideAnimations"/> (delay + duration) has
/// completed.
/// </summary>
public void Hide()
{
DispatcherQueue.TryEnqueue(
DispatcherQueuePriority.Low,
() =>
{
if (Content is UIElement content)
{
content.Visibility = Visibility.Collapsed;
}
_hideCloseTimer.Debounce(
AppWindow.Hide,
interval: GetAnimationSetTotalDuration(_hideAnimations),
immediate: false);
});
}
private static TimeSpan GetAnimationSetTotalDuration(ImplicitAnimationSet set)
{
TimeSpan longest = TimeSpan.Zero;
foreach (var animation in set)
{
if (animation is Animation anim)
{
var total = (anim.Delay ?? TimeSpan.Zero) + (anim.Duration ?? TimeSpan.Zero);
if (total > longest)
{
longest = total;
}
}
}
return longest;
}
private void ApplyExStyleBit(int bit, bool set)
{
if (_hwnd == 0)
{
return;
}
nint exStyle = GetWindowLongPtr(_hwnd, GwlExStyle);
nint updated = set ? exStyle | bit : exStyle & ~(nint)bit;
if (updated != exStyle)
{
_ = SetWindowLongPtr(_hwnd, GwlExStyle, updated);
}
}
private static ImplicitAnimationSet BuildDefaultShowAnimations() => new()
{
new OpacityAnimation
{
From = 0,
To = 1.0,
Duration = TimeSpan.FromMilliseconds(200),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
EasingType = EasingType.Cubic,
},
new TranslationAnimation
{
From = "0,24,32",
To = "0,0,32",
Duration = TimeSpan.FromMilliseconds(250),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
EasingType = EasingType.Cubic,
},
};
private static ImplicitAnimationSet BuildDefaultHideAnimations() => new()
{
new OpacityAnimation
{
From = 1.0,
To = 0,
Duration = TimeSpan.FromMilliseconds(180),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
EasingType = EasingType.Cubic,
},
new TranslationAnimation
{
From = "0,0,32",
To = "0,12,32",
Duration = TimeSpan.FromMilliseconds(180),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
EasingType = EasingType.Cubic,
},
};
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
[LibraryImport("dwmapi.dll")]
private static unsafe partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, void* pvAttribute, int cbAttribute);
}

View File

@@ -1,68 +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.Collections.Generic;
using System.IO;
using System.Linq;
using ShortcutGuide.Helpers;
using ShortcutGuide.Models;
using YamlDotNet.Serialization;
// This class should be moved to WinGet in the future
namespace ShortcutGuide.IndexYmlGenerator
{
public class IndexYmlGenerator
{
public static void Main()
{
CreateIndexYmlFile();
}
// Todo: Exception handling
public static void CreateIndexYmlFile()
{
string path = ManifestInterpreter.PathOfManifestFiles;
if (File.Exists(Path.Combine(path, "index.yml")))
{
File.Delete(Path.Combine(path, "index.yml"));
}
IndexFile indexFile = new() { };
Dictionary<(string WindowFilter, bool BackgroundProcess), List<string>> processes = [];
foreach (string file in Directory.EnumerateFiles(path, "*.yml"))
{
string content = File.ReadAllText(file);
Deserializer deserializer = new();
ShortcutFile shortcutFile = deserializer.Deserialize<ShortcutFile>(content);
if (processes.TryGetValue((shortcutFile.WindowFilter, shortcutFile.BackgroundProcess), out List<string>? apps))
{
if (apps.Contains(shortcutFile.PackageName))
{
continue;
}
apps.Add(shortcutFile.PackageName);
continue;
}
processes[(shortcutFile.WindowFilter, shortcutFile.BackgroundProcess)] = [shortcutFile.PackageName];
}
indexFile.Index = processes.Select(item => new IndexFile.IndexItem
{
WindowFilter = item.Key.WindowFilter,
BackgroundProcess = item.Key.BackgroundProcess,
Apps = [.. item.Value],
}).ToArray();
// Todo: Take the default shell name from the settings or environment variable, default to "+WindowsNT.Shell"
indexFile.DefaultShellName = "+WindowsNT.Shell";
Serializer serializer = new();
string yamlContent = serializer.Serialize(indexFile);
File.WriteAllText(Path.Combine(path, "index.yml"), yamlContent);
}
}
}

View File

@@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>ShortcutGuide.IndexYmlGenerator</RootNamespace>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<AssemblyName>PowerToys.ShortcutGuide.IndexYmlGenerator</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ShortcutGuide.Ui\ShortcutGuide.Ui.csproj" />
</ItemGroup>
</Project>

View File

@@ -68,6 +68,65 @@ namespace ShortcutGuide.Helpers
private static IndexFile? cachedIndexFile;
private static DateTime cachedIndexLastWriteTimeUtc;
/// <summary>
/// Scans the per-user manifest folder and (re)writes <c>index.yml</c>,
/// which maps window filters / background-process flags to the list of
/// app manifest IDs that match. Runs in-process; safe to call from a
/// background thread.
/// </summary>
public static void GenerateIndexYmlFile()
{
string path = PathOfManifestFiles;
string indexPath = Path.Combine(path, "index.yml");
if (File.Exists(indexPath))
{
File.Delete(indexPath);
}
IndexFile indexFile = default;
Dictionary<(string WindowFilter, bool BackgroundProcess), List<string>> processes = [];
foreach (string file in Directory.EnumerateFiles(path, "*.yml"))
{
string content = File.ReadAllText(file);
Deserializer deserializer = new();
ShortcutFile shortcutFile = deserializer.Deserialize<ShortcutFile>(content);
if (processes.TryGetValue((shortcutFile.WindowFilter, shortcutFile.BackgroundProcess), out List<string>? apps))
{
if (apps.Contains(shortcutFile.PackageName))
{
continue;
}
apps.Add(shortcutFile.PackageName);
continue;
}
processes[(shortcutFile.WindowFilter, shortcutFile.BackgroundProcess)] = [shortcutFile.PackageName];
}
indexFile.Index = processes.Select(item => new IndexFile.IndexItem
{
WindowFilter = item.Key.WindowFilter,
BackgroundProcess = item.Key.BackgroundProcess,
Apps = [.. item.Value],
}).ToArray();
// Todo: Take the default shell name from the settings or environment variable, default to "+WindowsNT.Shell"
indexFile.DefaultShellName = "+WindowsNT.Shell";
Serializer serializer = new();
File.WriteAllText(indexPath, serializer.Serialize(indexFile));
// Invalidate the cache so subsequent reads pick up the fresh index.
lock (IndexLock)
{
cachedIndexFile = null;
cachedIndexLastWriteTimeUtc = default;
}
}
/// <summary>
/// Retrieves the index YAML file that contains the list of all applications and their shortcuts from the cache.
/// </summary>
@@ -102,15 +161,21 @@ namespace ShortcutGuide.Helpers
/// <summary>
/// Retrieves all application IDs that should be displayed, based on the foreground window and background processes.
/// </summary>
/// <param name="foregroundWindow">
/// HWND of the window to treat as the active foreground app. Pass the
/// value captured at Shortcut Guide startup so we match the user's
/// target app and not the Shortcut Guide window itself. If <c>0</c>,
/// the current foreground window is queried (legacy behavior).
/// </param>
/// <returns>
/// A dictionary mapping each application ID to the full path of the executable
/// that caused the match (used for icon extraction), or <c>null</c> when no
/// specific executable is associated (for example, wildcard filters like the
/// default shell).
/// </returns>
public static Dictionary<string, string?> GetAllCurrentApplicationIds()
public static Dictionary<string, string?> GetAllCurrentApplicationIds(nint foregroundWindow = 0)
{
nint handle = NativeMethods.GetForegroundWindow();
nint handle = foregroundWindow != 0 ? foregroundWindow : NativeMethods.GetForegroundWindow();
Dictionary<string, string?> applicationIds = new(StringComparer.Ordinal);

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using ManagedCommon;
@@ -21,9 +20,20 @@ namespace ShortcutGuide
{
public static Thread CopyAndIndexGenerationThread { get; private set; } = null!;
/// <summary>
/// HWND of the window that was in the foreground when the Shortcut Guide
/// process started. Captured before any SG window is shown so that
/// shortcut lookup matches the user's target app and not SG itself.
/// </summary>
public static nint OriginalForegroundWindow { get; private set; }
[STAThread]
public static void Main(string[] args)
{
// Capture the foreground window FIRST, before logger init or any
// other work that might run a message pump and let focus shift.
OriginalForegroundWindow = NativeMethods.GetForegroundWindow();
Logger.InitializeLogger("\\ShortcutGuide\\Logs");
// The module interface passes: <powertoys_pid> [telemetry]
@@ -71,31 +81,13 @@ namespace ShortcutGuide
Logger.LogError($"Failed to copy bundled shortcut manifests from '{sourceManifestFolder}'.", ex);
}
string indexGeneratorPath = Path.Combine(
Path.GetDirectoryName(Environment.ProcessPath)!,
"PowerToys.ShortcutGuide.IndexYmlGenerator.exe");
try
{
using Process? indexGeneration = Process.Start(indexGeneratorPath);
if (indexGeneration is null)
{
Logger.LogError($"Failed to start index generation process '{indexGeneratorPath}'.");
return;
}
indexGeneration.WaitForExit();
if (indexGeneration.ExitCode != 0)
{
Logger.LogError($"Index generation failed with exit code {indexGeneration.ExitCode}. There may be a corrupt shortcuts file in \"{ManifestInterpreter.PathOfManifestFiles}\".");
return;
}
ManifestInterpreter.GenerateIndexYmlFile();
}
catch (Exception ex)
{
Logger.LogError($"Failed to start or wait for index generation process '{indexGeneratorPath}'.", ex);
Logger.LogError($"Index generation failed. There may be a corrupt shortcuts file in \"{ManifestInterpreter.PathOfManifestFiles}\".", ex);
return;
}

View File

@@ -100,6 +100,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />

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.Collections.Generic;
using System.IO;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
@@ -24,7 +26,10 @@ namespace ShortcutGuide
internal static MainWindow MainWindow { get; private set; } = null!;
internal static TaskbarWindow TaskBarWindow { get; private set; } = null!;
// May remain null if the taskbar overlay window failed to initialize
// (e.g. shell automation API failures on some configurations). The
// main window must remain usable in that case.
internal static TaskbarWindow? TaskBarWindow { get; private set; }
internal static string CurrentAppName { get; set; } = string.Empty;
@@ -37,14 +42,25 @@ namespace ShortcutGuide
{
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
try
{
TaskBarWindow = new TaskbarWindow();
}
catch (Exception ex)
{
// Taskbar overlay is non-critical chrome; keep the main window usable.
Logger.LogError("Failed to construct the ShortcutGuide TaskbarWindow; continuing without taskbar overlay.", ex);
TaskBarWindow = null;
}
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
TaskBarWindow?.Close();
};
}

View File

@@ -10,7 +10,7 @@
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="MinHeight" Value="16" />
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />

View File

@@ -12,7 +12,7 @@
x:Name="IndicatorRectangle"
Width="36"
Height="36"
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
Background="{ThemeResource ControlFillColorDefaultBrush}"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
BorderThickness="1"
CornerRadius="{StaticResource ControlCornerRadius}">

View File

@@ -1,70 +1,56 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
<?xml version="1.0" encoding="utf-8" ?>
<common:TransparentWindow
x:Class="ShortcutGuide.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="using:Microsoft.PowerToys.Common.UI.Controls.Window"
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"
Width="610"
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>
<Grid x:Name="MainPage" Background="Transparent">
<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>
<!-- If footer only has one item, a visual bug can occur. This fake settings button will have set height to zero as soon as the content is loaded -->
<NavigationViewItem
x:Name="FakeSettingsButton"
Icon="Setting"
SelectsOnInvoked="False"
Tag="Settings"
Tapped="Settings_Tapped" />
<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>
<NavigationView
x:Name="WindowSelector"
Grid.Row="1"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
SelectionChanged="WindowSelector_SelectionChanged"
Style="{StaticResource RailNavigationViewStyle}">
<NavigationView.MenuItems />
<NavigationView.FooterMenuItems>
<!-- If footer only has one item, a visual bug can occur. This fake settings button will have set height to zero as soon as the content is loaded -->
<NavigationViewItem
x:Name="FakeSettingsButton"
Icon="Setting"
SelectsOnInvoked="False"
Tag="Settings"
Tapped="Settings_Tapped" />
<NavigationViewItem
x:Uid="SettingsButton"
Icon="Setting"
SelectsOnInvoked="False"
Tag="Settings"
Tapped="Settings_Tapped" />
</NavigationView.FooterMenuItems>
<NavigationView.Content>
<Frame x:Name="ContentFrame" Background="Transparent" />
</NavigationView.Content>
<!--<NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
</NavigationView.Resources>-->
</NavigationView>
</Grid>
</common:TransparentWindow>

View File

@@ -9,12 +9,17 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Common.UI;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Animations;
using ManagedCommon;
using Microsoft.PowerToys.Common.UI.Controls.Window;
using Microsoft.PowerToys.Settings.UI.Library;
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.Animation;
using Microsoft.UI.Xaml.Media.Imaging;
using ShortcutGuide.Helpers;
using ShortcutGuide.Models;
@@ -29,14 +34,19 @@ using WinUIEx.Messaging;
namespace ShortcutGuide
{
public sealed partial class MainWindow : WindowEx, IDisposable
public sealed partial class MainWindow : TransparentWindow, IDisposable
{
private const int SlideInDurationMs = 280;
private const int SlideOutDurationMs = 200;
private readonly Stopwatch _sessionStopwatch = Stopwatch.StartNew();
private readonly Task<Dictionary<string, string?>> _getAppIdsTask;
private readonly Microsoft.UI.Dispatching.DispatcherQueueTimer _slideOutTimer = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread().CreateTimer();
private Dictionary<string, string?> _currentApplicationIds = [];
private ShortcutFile? _shortcutFile;
private string _selectedAppName = null!;
private string _closeType = "Unknown";
private bool _isClosing;
internal long SessionDurationMs => _sessionStopwatch.ElapsedMilliseconds;
@@ -51,16 +61,14 @@ namespace ShortcutGuide
_getAppIdsTask = Task.Run(() =>
{
Program.CopyAndIndexGenerationThread.Join();
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds();
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds(Program.OriginalForegroundWindow);
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) =>
@@ -81,7 +89,7 @@ namespace ShortcutGuide
if (e.Key == VirtualKey.Escape)
{
_closeType = "Escape";
Close();
SlideOutAndClose();
}
};
@@ -102,6 +110,17 @@ namespace ShortcutGuide
Logger.LogError("Invalid theme value in settings: " + App.ShortcutGuideProperties.Theme.Value);
break;
}
// Inset the card from the window edges so the shadow has room to
// render and the visible chrome doesn't touch the screen edge.
this.Card.Margin = new Thickness(12);
// Position the window before it's shown so the user doesn't see it
// flash at the default location, then attach the slide animations
// and start the card collapsed so Activate() triggers the slide-in.
this.SetWindowPosition();
AttachSlideAnimations();
this.Card.Visibility = Visibility.Collapsed;
}
protected override void OnStateChanged(WindowState state)
@@ -123,7 +142,7 @@ namespace ShortcutGuide
{
#if !DEBUG
_closeType = "Deactivated";
Close();
SlideOutAndClose();
#endif
}
@@ -133,7 +152,6 @@ namespace ShortcutGuide
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)
{
Content.GettingFocus += (_, _) =>
@@ -142,7 +160,6 @@ namespace ShortcutGuide
this.FakeSettingsButton.Height = 0;
};
this.SetWindowPosition();
this._setPosition = true;
AppWindow.Changed += (_, a) =>
@@ -154,11 +171,93 @@ namespace ShortcutGuide
this.SetWindowPosition();
};
// Trigger the slide-in by flipping the card from Collapsed to
// Visible; the implicit Show animations attached in the ctor
// take it from off-screen to its final position.
this.Card.Visibility = Visibility.Visible;
}
_ = this.InitializeNavItemsAsync();
}
private void AttachSlideAnimations()
{
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
// Slide in from off-screen on the same edge the window is pinned to.
// Width is in DIPs, which is what TranslationAnimation expects.
var offset = windowPosition == ShortcutGuideWindowPosition.Right ? Width : -Width;
var offsetString = $"{offset.ToString(System.Globalization.CultureInfo.InvariantCulture)},0,0";
var showAnimations = new ImplicitAnimationSet
{
new OpacityAnimation
{
From = 0,
To = 1.0,
Duration = TimeSpan.FromMilliseconds(SlideInDurationMs),
EasingMode = EasingMode.EaseOut,
EasingType = EasingType.Cubic,
},
new TranslationAnimation
{
From = offsetString,
To = "0,0,0",
Duration = TimeSpan.FromMilliseconds(SlideInDurationMs),
EasingMode = EasingMode.EaseOut,
EasingType = EasingType.Cubic,
},
};
var hideAnimations = new ImplicitAnimationSet
{
new OpacityAnimation
{
From = 1.0,
To = 0,
Duration = TimeSpan.FromMilliseconds(SlideOutDurationMs),
EasingMode = EasingMode.EaseIn,
EasingType = EasingType.Cubic,
},
new TranslationAnimation
{
From = "0,0,0",
To = offsetString,
Duration = TimeSpan.FromMilliseconds(SlideOutDurationMs),
EasingMode = EasingMode.EaseIn,
EasingType = EasingType.Cubic,
},
};
Implicit.SetShowAnimations(this.Card, showAnimations);
Implicit.SetHideAnimations(this.Card, hideAnimations);
}
private void SlideOutAndClose()
{
if (_isClosing)
{
return;
}
_isClosing = true;
// If we never finished the slide-in (e.g. Deactivated fires before
// first activation completes), just close immediately.
if (!_setPosition || this.Card.Visibility == Visibility.Collapsed)
{
Close();
return;
}
this.Card.Visibility = Visibility.Collapsed;
_slideOutTimer.Debounce(
Close,
interval: TimeSpan.FromMilliseconds(SlideOutDurationMs),
immediate: false);
}
private async Task InitializeNavItemsAsync()
{
try
@@ -170,7 +269,7 @@ namespace ShortcutGuide
{
Logger.LogError("Failed to initialize navigation items.", ex);
_closeType = "InitializationFailed";
this.DispatcherQueue.TryEnqueue(() => this.Close());
this.DispatcherQueue.TryEnqueue(() => this.SlideOutAndClose());
}
}
@@ -236,12 +335,12 @@ namespace ShortcutGuide
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;
var taskbarWindow = App.TaskBarWindow?.AppWindow;
bool taskbarOnLeft = taskbarWindow is not null && taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
bool taskbarOnRight = taskbarWindow is not null && taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
double newHeight = monitorRect.Height / dpi;
if (taskbarOnLeft || taskbarOnRight)
if ((taskbarOnLeft || taskbarOnRight) && taskbarWindow is not null)
{
newHeight -= taskbarWindow.Size.Height;
}
@@ -273,14 +372,14 @@ namespace ShortcutGuide
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
App.TaskBarWindow.Hide();
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();
App.TaskBarWindow?.Activate();
}
// Reposition before navigating so the taskbar window does not clip into the main window.

View File

@@ -1,36 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
<common:TransparentWindow
x:Class="ShortcutGuide.ShortcutGuideXAML.TaskbarWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="using:Microsoft.PowerToys.Common.UI.Controls.Window"
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" />
<Canvas
x:Name="KeyHolder"
Grid.Column="1"
Margin="0,4,0,0" />
<StackPanel
Margin="8"
VerticalAlignment="Top"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<Border
@@ -54,4 +50,4 @@
</StackPanel>
</Grid>
</winuiex:WindowEx>
</common:TransparentWindow>

View File

@@ -4,8 +4,13 @@
using System;
using System.Globalization;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Animations;
using ManagedCommon;
using Microsoft.PowerToys.Common.UI.Controls.Window;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using ShortcutGuide.Controls;
using ShortcutGuide.Helpers;
using Windows.Foundation;
@@ -15,8 +20,18 @@ using static ShortcutGuide.NativeMethods;
namespace ShortcutGuide.ShortcutGuideXAML
{
public sealed partial class TaskbarWindow : WindowEx
public sealed partial class TaskbarWindow : TransparentWindow
{
private const int SlideInDurationMs = 240;
private const int SlideOutDurationMs = 180;
// Card padding inside the transparent window so the acrylic chrome's
// shadow / corners have room to render. Matches MainWindow.
private const double CardMargin = 12;
// Fallback animation offset until the first measurement-based attach runs.
private const double DefaultSlideOffset = 90;
private float DPI => DpiHelper.GetDPIScaleForWindow(WindowNative.GetWindowHandle(this));
private Rect WorkArea => DisplayHelper.GetWorkAreaForDisplayWithWindow(WindowNative.GetWindowHandle(this));
@@ -24,10 +39,65 @@ namespace ShortcutGuide.ShortcutGuideXAML
public TaskbarWindow()
{
this.InitializeComponent();
this.Card.Margin = new Thickness(CardMargin);
AttachSlideAnimations(DefaultSlideOffset);
this.UpdateTasklistButtons();
this.Activated += (_, _) => this.UpdateTasklistButtons();
}
private void AttachSlideAnimations(double offsetDips)
{
// Slide up from below — the numbers window sits just above the
// taskbar, so a vertical slide from the bottom edge reads best.
var offsetString = $"0,{offsetDips.ToString(CultureInfo.InvariantCulture)},0";
var showAnimations = new ImplicitAnimationSet
{
new OpacityAnimation
{
From = 0,
To = 1.0,
Duration = TimeSpan.FromMilliseconds(SlideInDurationMs),
EasingMode = EasingMode.EaseOut,
EasingType = EasingType.Cubic,
},
new TranslationAnimation
{
From = offsetString,
To = "0,0,0",
Duration = TimeSpan.FromMilliseconds(SlideInDurationMs),
EasingMode = EasingMode.EaseOut,
EasingType = EasingType.Cubic,
},
};
var hideAnimations = new ImplicitAnimationSet
{
new OpacityAnimation
{
From = 1.0,
To = 0,
Duration = TimeSpan.FromMilliseconds(SlideOutDurationMs),
EasingMode = EasingMode.EaseIn,
EasingType = EasingType.Cubic,
},
new TranslationAnimation
{
From = "0,0,0",
To = offsetString,
Duration = TimeSpan.FromMilliseconds(SlideOutDurationMs),
EasingMode = EasingMode.EaseIn,
EasingType = EasingType.Cubic,
},
};
Implicit.SetShowAnimations(this.Card, showAnimations);
Implicit.SetHideAnimations(this.Card, hideAnimations);
}
public void UpdateTasklistButtons()
{
// This move ensures the window spawns on the same monitor as the main window
@@ -51,8 +121,9 @@ namespace ShortcutGuide.ShortcutGuideXAML
float dpi = this.DPI;
double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value;
double windowHeight = 58;
double windowMargin = 8 * dpi;
double contentHeight = 58;
double windowHeight = contentHeight + (2 * CardMargin);
double windowMargin = CardMargin * dpi;
double windowWidth = windowsLogoColumnWidth;
double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi);
double yPosition = this.WorkArea.Bottom - (windowHeight * dpi);
@@ -78,6 +149,10 @@ namespace ShortcutGuide.ShortcutGuideXAML
this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight);
AppWindow.MoveInZOrderAtTop();
// Re-tune the slide distance to the actual final window height so
// the card glides up from just below the visible bottom edge.
AttachSlideAnimations(windowHeight);
}
}
}