mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-03 00:49:18 +02:00
Compare commits
8 Commits
powerscrip
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e02f68b286 | ||
|
|
ac557e220b | ||
|
|
d021b0550f | ||
|
|
fa29c5436f | ||
|
|
05691ee4df | ||
|
|
859dee310d | ||
|
|
595961b7a2 | ||
|
|
bda8300206 |
9
.github/actions/spell-check/expect.txt
vendored
9
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
289
src/common/Common.UI.Controls/Window/TransparentWindow.cs
Normal file
289
src/common/Common.UI.Controls/Window/TransparentWindow.cs
Normal 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><common:TransparentWindow><TextBlock/></common:TransparentWindow></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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user