mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-05 09:59:28 +02:00
Compare commits
2 Commits
main
...
dev/noraa-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2533c83f1b | ||
|
|
b2f5d1d1dd |
@@ -52,7 +52,7 @@ namespace ShortcutGuide.Converters
|
||||
if (description.Shift)
|
||||
{
|
||||
shortcutList.Add(16); // The Shift key or button.
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in description.Keys)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ShortcutGuide.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for Windows named event operations.
|
||||
/// Provides unified event signaling with consistent error handling and logging.
|
||||
/// </summary>
|
||||
public static class EventHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Signals a named event. Creates the event if it doesn't exist.
|
||||
/// </summary>
|
||||
/// <param name="eventName">The name of the event to signal.</param>
|
||||
/// <returns>True if the event was signaled successfully, false otherwise.</returns>
|
||||
public static bool SignalEvent(string eventName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(eventName))
|
||||
{
|
||||
Logger.LogWarning("[EventHelper] SignalEvent called with null or empty event name");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var eventHandle = new EventWaitHandle(
|
||||
false,
|
||||
EventResetMode.AutoReset,
|
||||
eventName);
|
||||
eventHandle.Set();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[EventHelper] Failed to signal event '{eventName}': {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ namespace ShortcutGuide.Helpers
|
||||
stream.Position = 0;
|
||||
|
||||
BitmapImage bitmapImage = new();
|
||||
bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
|
||||
bitmapImage.SetSource(stream.AsRandomAccessStream());
|
||||
return bitmapImage;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace ShortcutGuide.Helpers
|
||||
/// <returns>An array of the taskbar buttons.</returns>
|
||||
public static TasklistButton[] GetButtons()
|
||||
{
|
||||
var monitor = NativeMethods.MonitorFromWindow(WindowNative.GetWindowHandle(App.MainWindow), 0);
|
||||
var monitor = NativeMethods.MonitorFromWindow(WindowNative.GetWindowHandle(App.OverlayWindow), 0);
|
||||
nint ptr = NativeMethods.GetTasklistButtons(monitor, out int size);
|
||||
if (ptr == nint.Zero)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,12 @@ namespace ShortcutGuide;
|
||||
internal static partial class NativeMethods
|
||||
{
|
||||
internal const int GWL_STYLE = -16;
|
||||
internal const int GWL_EXSTYLE = -20;
|
||||
internal const int WS_CAPTION = 0x00C00000;
|
||||
internal const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
internal const int WS_EX_WINDOWEDGE = 0x00000100;
|
||||
internal const int WS_EX_CLIENTEDGE = 0x00000200;
|
||||
internal const int WS_EX_DLGMODALFRAME = 0x00000001;
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
@@ -29,6 +34,13 @@ internal static partial class NativeMethods
|
||||
[LibraryImport("User32.dll")]
|
||||
internal static partial IntPtr MonitorFromWindow(IntPtr hwnd, int dwFlags);
|
||||
|
||||
[LibraryImport("User32.dll")]
|
||||
internal static partial IntPtr MonitorFromPoint(POINT pt, int dwFlags);
|
||||
|
||||
[LibraryImport("User32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetMonitorInfoW(IntPtr hMonitor, ref MONITORINFO lpmi);
|
||||
|
||||
[LibraryImport("Shcore.dll")]
|
||||
internal static partial long GetDpiForMonitor(IntPtr hmonitor, int dpiType, ref int dpiX, ref int dpiY);
|
||||
|
||||
@@ -58,6 +70,58 @@ internal static partial class NativeMethods
|
||||
|
||||
internal delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial void CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, LPARAM lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern void SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs, int cbSize);
|
||||
|
||||
internal struct INPUT
|
||||
{
|
||||
public uint Type;
|
||||
public MOUSEKEYBDHARDWAREINPUT Data;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct MOUSEKEYBDHARDWAREINPUT
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public MOUSEINPUT Mouse;
|
||||
[FieldOffset(0)]
|
||||
public KEYBDINPUT Keyboard;
|
||||
[FieldOffset(0)]
|
||||
public HARDWAREINPUT Hardware;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MOUSEINPUT
|
||||
{
|
||||
public int Dx;
|
||||
public int Dy;
|
||||
public uint MouseData;
|
||||
public uint DwFlags;
|
||||
public uint Time;
|
||||
public IntPtr DwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct KEYBDINPUT
|
||||
{
|
||||
public ushort WVk;
|
||||
public ushort WScan;
|
||||
public uint DwFlags;
|
||||
public uint Time;
|
||||
public IntPtr DwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct HARDWAREINPUT
|
||||
{
|
||||
public uint Msg;
|
||||
public ushort ParamL;
|
||||
public ushort ParamH;
|
||||
}
|
||||
|
||||
internal struct LPARAM(IntPtr value)
|
||||
{
|
||||
internal IntPtr Value = value;
|
||||
@@ -103,6 +167,15 @@ internal static partial class NativeMethods
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MONITORINFO
|
||||
{
|
||||
public uint CbSize;
|
||||
public RECT RcMonitor;
|
||||
public RECT RcWork;
|
||||
public uint DwFlags;
|
||||
}
|
||||
|
||||
public enum MonitorFromWindowDwFlags
|
||||
{
|
||||
MONITOR_DEFAULTTONEAREST = 2,
|
||||
|
||||
@@ -21,13 +21,14 @@ namespace ShortcutGuide
|
||||
{
|
||||
public static Thread CopyAndIndexGenerationThread { get; private set; } = null!;
|
||||
|
||||
public static nint ForegroundWindowHandle { get; private set; } = nint.Zero;
|
||||
public static nint ForegroundWindowHandle { get; set; } = nint.Zero;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
ForegroundWindowHandle = NativeMethods.GetForegroundWindow();
|
||||
Logger.InitializeLogger("\\ShortcutGuide\\Logs");
|
||||
LogForegroundCapture(ForegroundWindowHandle);
|
||||
|
||||
// The module interface passes: <powertoys_pid> [telemetry]
|
||||
if (args.Length >= 2 && args[1] == "telemetry")
|
||||
@@ -156,5 +157,47 @@ namespace ShortcutGuide
|
||||
Logger.LogError("Failed to send settings telemetry.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the foreground window captured at startup so we can see in
|
||||
/// the SG log which app was foreground at the moment SG.exe began
|
||||
/// running. The dictionary populated from this HWND drives the order
|
||||
/// of nav items, so a wrong/empty capture surfaces as the wrong app
|
||||
/// being auto-selected.
|
||||
/// </summary>
|
||||
private static void LogForegroundCapture(nint hwnd)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (hwnd == nint.Zero)
|
||||
{
|
||||
Logger.LogInfo("Foreground capture: HWND=0 (no foreground window).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (NativeMethods.GetWindowThreadProcessId(hwnd, out uint processId) == 0)
|
||||
{
|
||||
Logger.LogInfo($"Foreground capture: HWND=0x{hwnd:X}; GetWindowThreadProcessId failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
string moduleName;
|
||||
try
|
||||
{
|
||||
using var proc = Process.GetProcessById((int)processId);
|
||||
moduleName = proc.MainModule?.ModuleName ?? "(null)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
moduleName = $"(failed: {ex.GetType().Name})";
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Foreground capture: HWND=0x{hwnd:X}, PID={processId}, Module={moduleName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to log foreground capture.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="ShortcutGuideXAML\Controls\BlankPage.xaml" />
|
||||
<None Remove="ShortcutGuideXAML\CustomNavigationViewStyle.xaml" />
|
||||
<None Remove="ShortcutGuideXAML\Pages\ShortcutsPage.xaml" />
|
||||
<None Remove="ShortcutGuideXAML\TaskbarIndicator.xaml" />
|
||||
<None Remove="ShortcutGuideXAML\TaskbarWindow.xaml" />
|
||||
<None Remove="ShortcutGuideXAML\OverlayWindow.xaml" />
|
||||
<None Remove="ShortcutGuideXAML\Controls\MainPaneControl.xaml" />
|
||||
<None Remove="ShortcutGuideXAML\Controls\TaskbarPaneControl.xaml" />
|
||||
<None Include="Assets\ShortcutGuide\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
@@ -110,7 +113,17 @@
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="ShortcutGuideXAML\TaskbarWindow.xaml">
|
||||
<Page Update="ShortcutGuideXAML\OverlayWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="ShortcutGuideXAML\Controls\MainPaneControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="ShortcutGuideXAML\Controls\TaskbarPaneControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
@@ -127,6 +140,11 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="ShortcutGuideXAML\Styles\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="ShortcutGuideXAML\Controls\BlankPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
|
||||
@@ -2,19 +2,28 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Windows.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using PowerToys.Interop;
|
||||
using ShortcutGuide.Models;
|
||||
using ShortcutGuide.ShortcutGuideXAML;
|
||||
using ShortcutGuide.Telemetry;
|
||||
using KeyEventHandler = Microsoft.UI.Xaml.Input.KeyEventHandler;
|
||||
|
||||
namespace ShortcutGuide
|
||||
{
|
||||
public partial class App
|
||||
public partial class App : IDisposable
|
||||
{
|
||||
internal static Dictionary<string, List<ShortcutEntry>> PinnedShortcuts { get; private set; } = new Dictionary<string, List<ShortcutEntry>>();
|
||||
|
||||
@@ -22,12 +31,23 @@ namespace ShortcutGuide
|
||||
|
||||
internal static ShortcutGuideProperties ShortcutGuideProperties { get; private set; } = null!;
|
||||
|
||||
internal static MainWindow MainWindow { get; private set; } = null!;
|
||||
/// <summary>
|
||||
/// The single transparent host that replaces the previous MainWindow +
|
||||
/// TaskbarWindow pair. The two surfaces are now XAML pseudo-windows
|
||||
/// inside this one window.
|
||||
/// </summary>
|
||||
internal static OverlayWindow OverlayWindow { get; private set; } = null!;
|
||||
|
||||
internal static TaskbarWindow TaskBarWindow { get; private set; } = null!;
|
||||
private HotkeySettingsControlHook _winKeyUpKeyboardHook = null!;
|
||||
|
||||
internal static string CurrentAppName { get; set; } = string.Empty;
|
||||
|
||||
private EventWaitHandle? _launchedEvent;
|
||||
|
||||
private Thread? _listenForLaunchedEventThread;
|
||||
|
||||
private static readonly UIntPtr _ignoreKeyEventFlag = 0x5557;
|
||||
|
||||
public App()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
@@ -35,17 +55,169 @@ namespace ShortcutGuide
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
this.LoadData();
|
||||
MainWindow = new MainWindow();
|
||||
TaskBarWindow = new TaskbarWindow();
|
||||
MainWindow.Activate();
|
||||
MainWindow.Closed += (_, _) =>
|
||||
this. LoadData();
|
||||
OverlayWindow = new OverlayWindow();
|
||||
OverlayWindow.Activate();
|
||||
OverlayWindow.AppWindow.Hide();
|
||||
OverlayWindow.Closed += (_, _) =>
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
|
||||
MainWindow.SessionDurationMs,
|
||||
MainWindow.CloseType));
|
||||
TaskBarWindow.Close();
|
||||
OverlayWindow.SessionDurationMs,
|
||||
OverlayWindow.CloseType));
|
||||
|
||||
// WinUI3's dispatcher loop does not terminate when the last
|
||||
// window closes; without Exit() the SG.exe process stays
|
||||
// alive, holds the AppInstance single-instance lock, and
|
||||
// blocks the next launch (the well-known "every other
|
||||
// long-press works" bug).
|
||||
Current.Exit();
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_launchedEvent = EventWaitHandle.OpenExisting(Constants.ShortcutGuideTriggerEvent());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to open existing event '{Constants.ShortcutGuideTriggerEvent()}': {ex.Message}");
|
||||
}
|
||||
|
||||
_listenForLaunchedEventThread = new Thread(ListenForLaunchedEvents)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "ShortcutGuide-ShowEventListener",
|
||||
};
|
||||
_listenForLaunchedEventThread.Start();
|
||||
_winKeyUpKeyboardHook = new HotkeySettingsControlHook(
|
||||
(int key) =>
|
||||
{
|
||||
SendSingleKeyboardInput((short)key, 0x0); // key down
|
||||
},
|
||||
(int key) =>
|
||||
{
|
||||
if (OverlayWindow.AppWindow.IsVisible)
|
||||
{
|
||||
OverlayWindow.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
OverlayWindow.CloseAnimated();
|
||||
});
|
||||
|
||||
NativeMethods.SendInput(1, [new() { Type = 1, Data = new() { Keyboard = new NativeMethods.KEYBDINPUT { WVk = 0xFF, DwFlags = 0x2 } } }], Marshal.SizeOf<NativeMethods.INPUT>());
|
||||
SendSingleKeyboardInput((short)key, 0x2); // key up
|
||||
}
|
||||
else
|
||||
{
|
||||
SendSingleKeyboardInput((short)key, 0x2); // key up
|
||||
}
|
||||
},
|
||||
() => true,
|
||||
(int key, nuint specialFlags) => key == 91 && specialFlags != _ignoreKeyEventFlag);
|
||||
}
|
||||
|
||||
private static bool IsExtendedVirtualKey(short vk)
|
||||
{
|
||||
return vk switch
|
||||
{
|
||||
0xA5 => true, // VK_RMENU (Right Alt - AltGr)
|
||||
0xA3 => true, // VK_RCONTROL
|
||||
0x2D => true, // VK_INSERT
|
||||
0x2E => true, // VK_DELETE
|
||||
0x23 => true, // VK_END
|
||||
0x24 => true, // VK_HOME
|
||||
0x21 => true, // VK_PRIOR (Page Up)
|
||||
0x22 => true, // VK_NEXT (Page Down)
|
||||
0x90 => true, // VK_NUMLOCK
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
|
||||
{
|
||||
if (IsExtendedVirtualKey(keyCode))
|
||||
{
|
||||
keyStatus |= 0x1; // KEYEVENTF_EXTENDEDKEY
|
||||
}
|
||||
|
||||
NativeMethods.INPUT input = new()
|
||||
{
|
||||
Type = 0x1, // INPUT_KEYBOARD
|
||||
Data = new NativeMethods.MOUSEKEYBDHARDWAREINPUT
|
||||
{
|
||||
Keyboard = new NativeMethods.KEYBDINPUT
|
||||
{
|
||||
WVk = (ushort)keyCode,
|
||||
DwFlags = keyStatus,
|
||||
DwExtraInfo = (nint)_ignoreKeyEventFlag,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
NativeMethods.INPUT[] inputs = [input];
|
||||
|
||||
NativeMethods.SendInput(1, inputs, Marshal.SizeOf<NativeMethods.INPUT>());
|
||||
}
|
||||
|
||||
private void ListenForLaunchedEvents()
|
||||
{
|
||||
if (_launchedEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var handles = new WaitHandle[] { _launchedEvent };
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var index = WaitHandle.WaitAny(handles);
|
||||
if (index == 0)
|
||||
{
|
||||
OverlayWindow.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
if (Keyboard.IsKeyDown(Key.LWin))
|
||||
{
|
||||
if (OverlayWindow.AppWindow.IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OverlayWindow.AppWindow.Show();
|
||||
OverlayWindow.MainPaneControl.Visibility = Visibility.Collapsed;
|
||||
OverlayWindow.AppWindow.MoveInZOrderAtTop();
|
||||
OverlayWindow.UpdateTaskbarPaneLayout();
|
||||
OverlayWindow.TaskbarPaneControl.Visibility = Visibility.Visible;
|
||||
return;
|
||||
}
|
||||
|
||||
if (OverlayWindow.AppWindow.IsVisible)
|
||||
{
|
||||
OverlayWindow.CloseAnimated();
|
||||
OverlayWindow.MainPaneControl.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.ForegroundWindowHandle = NativeMethods.GetForegroundWindow();
|
||||
OverlayWindow.MainPaneControl.Visibility = Visibility.Collapsed;
|
||||
OverlayWindow.AppWindow.Show();
|
||||
await OverlayWindow.MainPaneControl.Open();
|
||||
OverlayWindow.AppWindow.MoveInZOrderAtTop();
|
||||
OverlayWindow.UpdateTaskbarPaneLayout();
|
||||
OverlayWindow.MainPaneControl.Visibility = Visibility.Visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadData()
|
||||
@@ -76,5 +248,33 @@ namespace ShortcutGuide
|
||||
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
|
||||
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_launchedEvent?.Dispose();
|
||||
|
||||
if (_listenForLaunchedEventThread == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_listenForLaunchedEventThread.Join(TimeSpan.FromMilliseconds(250)))
|
||||
{
|
||||
_listenForLaunchedEventThread.Interrupt();
|
||||
_listenForLaunchedEventThread.Join(TimeSpan.FromMilliseconds(250));
|
||||
}
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
}
|
||||
catch (ThreadStateException)
|
||||
{
|
||||
}
|
||||
|
||||
_listenForLaunchedEventThread = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace ShortcutGuide.Backdrops;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="SystemBackdrop"/> that renders desktop acrylic and stays in
|
||||
/// the active visual state even when the hosting window is not activated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The built-in <see cref="DesktopAcrylicBackdrop"/> tracks the host window's
|
||||
/// <c>IsInputActive</c> state and falls back to a solid color whenever the
|
||||
/// window is not the foreground window. That makes it unusable for the
|
||||
/// per-card backdrops inside the Shortcut Guide overlay: the overlay window
|
||||
/// is technically the foreground, but the <c>SystemBackdropElement</c>s that
|
||||
/// host the per-pane acrylic are themselves not activated, and the standard
|
||||
/// backdrop greys out anyway when the overlay loses focus during the brief
|
||||
/// hand-off to elevated windows / context menus.
|
||||
///
|
||||
/// This backdrop drives a <see cref="DesktopAcrylicController"/> with a
|
||||
/// <see cref="SystemBackdropConfiguration"/> whose <c>IsInputActive</c> is
|
||||
/// permanently <see langword="true"/>, so the native acrylic effect is always
|
||||
/// rendered.
|
||||
///
|
||||
/// Mirrors the implementation in PR #48176 (CmdPal toast notification). Once
|
||||
/// that PR lands and the type is promoted into <c>Common.UI.Controls</c>,
|
||||
/// this local copy should be retired in favor of the shared one.
|
||||
/// </remarks>
|
||||
public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
{
|
||||
private readonly Dictionary<ICompositionSupportsSystemBackdrop, BackdropTarget> _targets = new();
|
||||
|
||||
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
|
||||
{
|
||||
base.OnTargetConnected(connectedTarget, xamlRoot);
|
||||
|
||||
var configuration = new SystemBackdropConfiguration
|
||||
{
|
||||
IsInputActive = true,
|
||||
Theme = ResolveTheme(xamlRoot),
|
||||
};
|
||||
|
||||
var controller = new DesktopAcrylicController();
|
||||
controller.SetSystemBackdropConfiguration(configuration);
|
||||
controller.AddSystemBackdropTarget(connectedTarget);
|
||||
|
||||
var target = new BackdropTarget(controller, configuration, xamlRoot);
|
||||
_targets[connectedTarget] = target;
|
||||
|
||||
if (xamlRoot.Content is FrameworkElement rootElement)
|
||||
{
|
||||
rootElement.ActualThemeChanged += target.OnActualThemeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
|
||||
{
|
||||
base.OnTargetDisconnected(disconnectedTarget);
|
||||
|
||||
if (_targets.Remove(disconnectedTarget, out var target))
|
||||
{
|
||||
if (target.XamlRoot.Content is FrameworkElement rootElement)
|
||||
{
|
||||
rootElement.ActualThemeChanged -= target.OnActualThemeChanged;
|
||||
}
|
||||
|
||||
target.Controller.RemoveSystemBackdropTarget(disconnectedTarget);
|
||||
target.Controller.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemBackdropTheme ResolveTheme(XamlRoot xamlRoot) =>
|
||||
xamlRoot.Content is FrameworkElement rootElement
|
||||
? rootElement.ActualTheme switch
|
||||
{
|
||||
ElementTheme.Dark => SystemBackdropTheme.Dark,
|
||||
ElementTheme.Light => SystemBackdropTheme.Light,
|
||||
_ => SystemBackdropTheme.Default,
|
||||
}
|
||||
: SystemBackdropTheme.Default;
|
||||
|
||||
private sealed class BackdropTarget
|
||||
{
|
||||
public BackdropTarget(DesktopAcrylicController controller, SystemBackdropConfiguration configuration, XamlRoot xamlRoot)
|
||||
{
|
||||
Controller = controller;
|
||||
Configuration = configuration;
|
||||
XamlRoot = xamlRoot;
|
||||
}
|
||||
|
||||
public DesktopAcrylicController Controller { get; }
|
||||
|
||||
public SystemBackdropConfiguration Configuration { get; }
|
||||
|
||||
public XamlRoot XamlRoot { get; }
|
||||
|
||||
public void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
Configuration.Theme = ResolveTheme(XamlRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Page
|
||||
x:Class="ShortcutGuide.ShortcutGuideXAML.Controls.BlankPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:ShortcutGuide.ShortcutGuideXAML.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<!-- Blank space to save memory -->
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace ShortcutGuide.ShortcutGuideXAML.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class BlankPage : Page
|
||||
{
|
||||
public BlankPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encod ing="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="ShortcutGuide.Controls.MainPaneControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:backdrops="using:ShortcutGuide.Backdrops"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<!--
|
||||
Pseudo-window: chrome (per-card acrylic + border + corner radius + shadow)
|
||||
is provided here in XAML instead of by a real WindowEx host. The outer Grid
|
||||
carries Translation Z=24 so its ThemeShadow projects onto the transparent
|
||||
overlay window behind it.
|
||||
-->
|
||||
<Grid
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Translation="0,0,24">
|
||||
<Grid.Shadow>
|
||||
<ThemeShadow />
|
||||
</Grid.Shadow>
|
||||
|
||||
<SystemBackdropElement CornerRadius="8">
|
||||
<SystemBackdropElement.SystemBackdrop>
|
||||
<backdrops:AlwaysActiveDesktopAcrylicBackdrop />
|
||||
</SystemBackdropElement.SystemBackdrop>
|
||||
</SystemBackdropElement>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="48" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid
|
||||
Padding="16,0"
|
||||
ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Width="16"
|
||||
Height="16"
|
||||
VerticalAlignment="Center"
|
||||
Source="/Assets/ShortcutGuide/ShortcutGuide.ico" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}" />
|
||||
<Button
|
||||
x:Name="CloseButton"
|
||||
x:Uid="CloseButton"
|
||||
Grid.Column="2"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Click="CloseButton_Click">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<NavigationView
|
||||
x:Name="WindowSelector"
|
||||
Grid.Row="1"
|
||||
IsBackButtonVisible="Collapsed"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
IsSettingsVisible="False"
|
||||
SelectionChanged="WindowSelector_SelectionChanged"
|
||||
Style="{StaticResource RailNavigationViewStyle}">
|
||||
<NavigationView.MenuItems />
|
||||
<NavigationView.FooterMenuItems>
|
||||
<NavigationViewItem
|
||||
x:Uid="SettingsButton"
|
||||
Icon="Setting"
|
||||
SelectsOnInvoked="False"
|
||||
Tag="Settings"
|
||||
Tapped="Settings_Tapped" />
|
||||
</NavigationView.FooterMenuItems>
|
||||
<NavigationView.Content>
|
||||
<Frame
|
||||
x:Name="ContentFrame"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1,1,0,0"
|
||||
CornerRadius="8,0,0,0" />
|
||||
</NavigationView.Content>
|
||||
<NavigationView.Resources>
|
||||
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
|
||||
</NavigationView.Resources>
|
||||
</NavigationView>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Common.UI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using ShortcutGuide.Helpers;
|
||||
using ShortcutGuide.Models;
|
||||
using ShortcutGuide.Pages;
|
||||
using ShortcutGuide.ShortcutGuideXAML.Controls;
|
||||
|
||||
namespace ShortcutGuide.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// The big shortcut-list pseudo-window that used to be <c>MainWindow</c>.
|
||||
/// Now a regular <see cref="UserControl"/> hosted inside
|
||||
/// <see cref="OverlayWindow"/>, so it can be sized / positioned / animated
|
||||
/// via XAML layout instead of by driving a real <c>WindowEx</c>.
|
||||
/// </summary>
|
||||
public sealed partial class MainPaneControl : UserControl
|
||||
{
|
||||
private Task<Dictionary<string, string?>>? _getAppIdsTask;
|
||||
private Dictionary<string, string?> _currentApplicationIds = [];
|
||||
private ShortcutFile? _shortcutFile;
|
||||
private string _selectedAppName = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever the user selects a different app in the nav list.
|
||||
/// The boolean payload indicates whether the newly-selected app
|
||||
/// exposes a <c><TASKBAR1-9></c> section, i.e. whether the
|
||||
/// overlay should reveal the taskbar number pseudo-window.
|
||||
/// </summary>
|
||||
public event EventHandler<bool>? SelectedAppTaskbarVisibilityChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user clicks the close button. The overlay window
|
||||
/// handles this the same way as pressing Escape.
|
||||
/// </summary>
|
||||
public event EventHandler? CloseRequested;
|
||||
|
||||
public MainPaneControl()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
Program.CopyAndIndexGenerationThread.Join();
|
||||
this.TitleTextBlock.Text = ResourceLoaderInstance.ResourceLoader.GetString("Title");
|
||||
|
||||
this.Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
public async Task Open()
|
||||
{
|
||||
// Same background work the original MainWindow ran in its
|
||||
// constructor: wait for the index-generation thread to finish
|
||||
// and then enumerate the apps to populate the nav list.
|
||||
_getAppIdsTask = Task.Run(async () =>
|
||||
{
|
||||
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds(Program.ForegroundWindowHandle);
|
||||
return _currentApplicationIds;
|
||||
});
|
||||
|
||||
await InitializeNavItemsAsync();
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (this.ContentFrame.Content is ShortcutsPage currentPage)
|
||||
{
|
||||
currentPage.Rows.Clear();
|
||||
}
|
||||
|
||||
this.ContentFrame.Navigate(typeof(BlankPage));
|
||||
|
||||
if (this.ContentFrame.BackStack != null)
|
||||
{
|
||||
this.ContentFrame.BackStack.Clear();
|
||||
}
|
||||
|
||||
this.ContentFrame.Content = null;
|
||||
|
||||
foreach (var item in this.WindowSelector.MenuItems.OfType<NavigationViewItem>())
|
||||
{
|
||||
if (item.Icon is ImageIcon imageIcon)
|
||||
{
|
||||
imageIcon.Source = null;
|
||||
}
|
||||
|
||||
if (item.Icon is PathIcon pathIcon)
|
||||
{
|
||||
pathIcon.Data = null;
|
||||
}
|
||||
|
||||
item.Icon = null;
|
||||
item.Content = null;
|
||||
}
|
||||
|
||||
this.WindowSelector.MenuItems.Clear();
|
||||
|
||||
_shortcutFile = null;
|
||||
_currentApplicationIds.Clear();
|
||||
|
||||
_getAppIdsTask?.Dispose();
|
||||
_getAppIdsTask = null;
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
internal string SelectedAppName => _selectedAppName;
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_getAppIdsTask?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits the background app-id enumeration and populates the nav
|
||||
/// list. Safe to call repeatedly: the work only runs once.
|
||||
/// </summary>
|
||||
private async Task InitializeNavItemsAsync()
|
||||
{
|
||||
if (_getAppIdsTask == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_currentApplicationIds = await _getAppIdsTask.ConfigureAwait(true);
|
||||
this.SetNavItems();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to initialize navigation items.", ex);
|
||||
|
||||
// Surface the failure so the overlay can close itself,
|
||||
// mirroring the original MainWindow.InitializeNavItemsAsync
|
||||
// behavior.
|
||||
this.DispatcherQueue.TryEnqueue(() => InitializationFailed?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the background app-id enumeration fails. The overlay
|
||||
/// listens to this so it can close itself (same behavior the
|
||||
/// original <c>MainWindow.InitializeNavItemsAsync</c> hand-coded).
|
||||
/// </summary>
|
||||
public event EventHandler? InitializationFailed;
|
||||
|
||||
private IconElement BuildNavIcon(string? executablePath)
|
||||
{
|
||||
// FIX: Use placeholder initially to reduce upfront memory
|
||||
// Icons can be loaded on SelectionChanged if needed
|
||||
return new FontIcon { Glyph = "\uEB91" };
|
||||
}
|
||||
|
||||
private void SetNavItems()
|
||||
{
|
||||
this.WindowSelector.MenuItems.Clear();
|
||||
if (this.WindowSelector.MenuItems.Count != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string defaultShellName = ManifestInterpreter.GetCachedIndexYamlFile().DefaultShellName;
|
||||
|
||||
foreach (var (item, executablePath) in this._currentApplicationIds)
|
||||
{
|
||||
if (item == defaultShellName)
|
||||
{
|
||||
var pathData = (string)Application.Current.Resources["WindowsLogoPathData"];
|
||||
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = "Windows", Icon = CreatePathIcon(pathData) });
|
||||
}
|
||||
else if (item == "Microsoft.PowerToys")
|
||||
{
|
||||
var pathData = (string)Application.Current.Resources["PowerToysLogoPathData"];
|
||||
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = CreatePathIcon(pathData) });
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
IconElement icon = BuildNavIcon(executablePath);
|
||||
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = icon });
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogError($"Failed to build nav item for application '{item}' (executable '{executablePath}').", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.WindowSelector.MenuItems.Count > 0)
|
||||
{
|
||||
this.WindowSelector.SelectedItem = this.WindowSelector.MenuItems[0];
|
||||
}
|
||||
}
|
||||
|
||||
private static PathIcon CreatePathIcon(string pathData)
|
||||
{
|
||||
var geometry = (Geometry)XamlBindingHelper.ConvertValue(typeof(Geometry), pathData);
|
||||
return new PathIcon
|
||||
{
|
||||
Data = geometry,
|
||||
Width = 20,
|
||||
Height = 20,
|
||||
};
|
||||
}
|
||||
|
||||
private void WindowSelector_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.SelectedItem is not NavigationViewItem selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItem.Icon is FontIcon && _currentApplicationIds.TryGetValue(selectedItem.Name, out string? exePath))
|
||||
{
|
||||
BitmapImage? bitmap = IconHelper.TryGetExecutableIcon(exePath);
|
||||
if (bitmap is not null)
|
||||
{
|
||||
selectedItem.Icon = new ImageIcon { Source = bitmap };
|
||||
}
|
||||
}
|
||||
|
||||
this._selectedAppName = selectedItem.Name;
|
||||
App.CurrentAppName = this._selectedAppName;
|
||||
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
|
||||
|
||||
bool exposesTaskbarSection = false;
|
||||
|
||||
if (this._shortcutFile is ShortcutFile file)
|
||||
{
|
||||
exposesTaskbarSection = file.Shortcuts is not null &&
|
||||
file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true);
|
||||
|
||||
if (this.ContentFrame.Content is ShortcutsPage currentPage)
|
||||
{
|
||||
currentPage.ClearData();
|
||||
}
|
||||
|
||||
this.ContentFrame.Navigate(
|
||||
typeof(ShortcutsPage),
|
||||
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
|
||||
}
|
||||
|
||||
SelectedAppTaskbarVisibilityChanged?.Invoke(this, exposesTaskbarSection);
|
||||
}
|
||||
|
||||
private void Settings_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ShortcutGuide);
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
Visibility="{x:Bind Shortcut.Description, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
<ItemsControl
|
||||
x:Name="OuterItemsControl"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind Shortcut.Shortcut, Mode=OneWay}">
|
||||
@@ -44,7 +45,7 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:ShortcutDescription">
|
||||
<ItemsControl ItemsSource="{Binding Converter={StaticResource ShortcutDescriptionToKeysConverter}}">
|
||||
<ItemsControl x:Name="InnerItemsControl" ItemsSource="{Binding Converter={StaticResource ShortcutDescriptionToKeysConverter}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using ShortcutGuide.Helpers;
|
||||
using ShortcutGuide.Models;
|
||||
|
||||
@@ -27,6 +29,81 @@ namespace ShortcutGuide.Controls
|
||||
public ShortcutItemView()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ClearNestedItemsControls(this.OuterItemsControl);
|
||||
|
||||
this.OuterItemsControl.ItemsSource = null;
|
||||
|
||||
if (this.ContextFlyout is MenuFlyout flyout)
|
||||
{
|
||||
flyout.Opening -= PinFlyout_Opening!;
|
||||
}
|
||||
|
||||
this.ContextFlyout = null;
|
||||
this.ClearValue(ShortcutProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively clears ItemsSource for all nested ItemsControl instances
|
||||
/// to break circular references and release converter-generated collections
|
||||
/// </summary>
|
||||
private void ClearNestedItemsControls(ItemsControl parentControl)
|
||||
{
|
||||
if (parentControl == null || parentControl.Items == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate through realized items
|
||||
for (int i = 0; i < parentControl.Items.Count; i++)
|
||||
{
|
||||
// Get the container for this item
|
||||
var container = parentControl.ContainerFromIndex(i) as FrameworkElement;
|
||||
if (container == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find any nested ItemsControl in the visual tree
|
||||
var nestedItemsControls = FindVisualChildren<ItemsControl>(container);
|
||||
foreach (var nestedControl in nestedItemsControls)
|
||||
{
|
||||
ClearNestedItemsControls(nestedControl);
|
||||
nestedControl.ItemsSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all visual children of a specific type in the visual tree
|
||||
/// </summary>
|
||||
private static System.Collections.Generic.IEnumerable<T> FindVisualChildren<T>(DependencyObject parent)
|
||||
where T : DependencyObject
|
||||
{
|
||||
if (parent == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
int childCount = VisualTreeHelper.GetChildrenCount(parent);
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
|
||||
if (child is T typedChild)
|
||||
{
|
||||
yield return typedChild;
|
||||
}
|
||||
|
||||
foreach (var descendant in FindVisualChildren<T>(child))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PinFlyout_Opening(object sender, object e)
|
||||
|
||||
@@ -3,27 +3,89 @@
|
||||
x:Class="ShortcutGuide.Controls.TaskbarIndicator"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:backdrops="using:ShortcutGuide.Backdrops"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:ShortcutGuide"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<!--
|
||||
Each indicator is a standalone tooltip-style element with a body
|
||||
(rounded rect) and a downward-pointing triangle tail, similar to
|
||||
TeachingTip's visual. The tail points toward the corresponding
|
||||
taskbar button below.
|
||||
-->
|
||||
<Grid
|
||||
HorizontalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!--
|
||||
Windows 11 system flyout animation: slide up + fade from the
|
||||
taskbar edge. Same timing as Action Center / Widgets pane
|
||||
(~367ms entrance, ~200ms exit). No scale.
|
||||
-->
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.367" />
|
||||
<animations:TranslationAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Cubic"
|
||||
From="0,12,0"
|
||||
To="0,0,0"
|
||||
Duration="0:0:0.367" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="0:0:0.200" />
|
||||
<animations:TranslationAnimation
|
||||
EasingMode="EaseIn"
|
||||
EasingType="Cubic"
|
||||
From="0,0,0"
|
||||
To="0,12,0"
|
||||
Duration="0:0:0.200" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
|
||||
<!-- Body -->
|
||||
<Border
|
||||
x:Name="IndicatorRectangle"
|
||||
Width="36"
|
||||
Height="36"
|
||||
MinWidth="40"
|
||||
MinHeight="40"
|
||||
Padding="8"
|
||||
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}">
|
||||
CornerRadius="8"
|
||||
Translation="0,0,32">
|
||||
<Border.Shadow>
|
||||
<ThemeShadow />
|
||||
</Border.Shadow>
|
||||
<TextBlock
|
||||
x:Name="IndicatorText"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontSize="16"
|
||||
Style="{ThemeResource BodyStrongTextBlockStyle}"
|
||||
Text="{x:Bind Label, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<!-- Triangle tail pointing down — only the two diagonal edges are
|
||||
stroked (no top line), so it seamlessly blends with the body
|
||||
border above. The negative margin overlaps the body's bottom
|
||||
border by 1px to hide the seam. -->
|
||||
<Path
|
||||
Grid.Row="1"
|
||||
Margin="0,-1,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Data="M 0,0 L 6,6 L 12,0"
|
||||
Fill="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
|
||||
Stroke="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="ShortcutGuide.Controls.TaskbarPaneControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<!--
|
||||
No shared background bar. Each indicator is a standalone tooltip
|
||||
positioned directly above its corresponding taskbar button.
|
||||
-->
|
||||
<Canvas x:Name="KeyHolder" />
|
||||
</UserControl>
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using ShortcutGuide.Helpers;
|
||||
using static ShortcutGuide.NativeMethods;
|
||||
|
||||
namespace ShortcutGuide.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// The taskbar number-indicator pseudo-window that used to be
|
||||
/// <c>TaskbarWindow</c>. Now a regular <see cref="UserControl"/> hosted
|
||||
/// inside <see cref="OverlayWindow"/>; the overlay applies the values
|
||||
/// returned from <see cref="UpdateTasklistButtons"/> to position the
|
||||
/// control inside its <see cref="Canvas"/>.
|
||||
/// </summary>
|
||||
public sealed partial class TaskbarPaneControl : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// The window-relative layout the overlay should apply to this control.
|
||||
/// All values are in DIPs.
|
||||
/// </summary>
|
||||
public readonly record struct TaskbarPaneLayout(double Left, double Top, double Width, double Height);
|
||||
|
||||
public TaskbarPaneControl()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the indicator children from the current taskbar buttons
|
||||
/// and returns the desired position/size the overlay should apply.
|
||||
/// Returns <see langword="null"/> when there are no taskbar buttons
|
||||
/// (caller should hide the control).
|
||||
/// </summary>
|
||||
/// <param name="overlayPhysicalOriginX">The overlay window's physical left in screen coordinates.</param>
|
||||
/// <param name="overlayPhysicalOriginY">The overlay window's physical top in screen coordinates.</param>
|
||||
/// <param name="dpi">DPI scale factor of the host overlay window.</param>
|
||||
/// <param name="workAreaBottomPhysical">Bottom of the work area in physical pixels.</param>
|
||||
public TaskbarPaneLayout? UpdateTasklistButtons(int overlayPhysicalOriginX, int overlayPhysicalOriginY, float dpi, double workAreaBottomPhysical)
|
||||
{
|
||||
TasklistButton[] buttons = [];
|
||||
try
|
||||
{
|
||||
buttons = TasklistPositions.GetButtons();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
|
||||
}
|
||||
|
||||
if (buttons.Length == 0)
|
||||
{
|
||||
this.KeyHolder.Children.Clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Each indicator is a standalone tooltip (~36px body + ~6px triangle
|
||||
// tail). Position the pane so that the bottom of the triangle sits
|
||||
// 8px above the bottom of the work area (taskbar edge).
|
||||
const double IndicatorBodyDip = 40;
|
||||
const double TriangleTailDip = 6;
|
||||
const double BottomMarginDip = 8;
|
||||
double indicatorTotalHeightDip = IndicatorBodyDip + TriangleTailDip;
|
||||
|
||||
double paneOriginPhysicalY = workAreaBottomPhysical - ((indicatorTotalHeightDip + BottomMarginDip) * dpi);
|
||||
|
||||
this.KeyHolder.Children.Clear();
|
||||
|
||||
double leftmostPhysicalX = buttons[0].X;
|
||||
double rightmostPhysicalX = buttons[0].X + buttons[0].Width;
|
||||
|
||||
foreach (TasklistButton b in buttons)
|
||||
{
|
||||
TaskbarIndicator indicator = new()
|
||||
{
|
||||
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
|
||||
};
|
||||
|
||||
this.KeyHolder.Children.Add(indicator);
|
||||
|
||||
// Center each indicator over its button
|
||||
double buttonCenterPhysical = b.X + (b.Width / 2.0);
|
||||
double indicatorLeftDip = ((buttonCenterPhysical - leftmostPhysicalX) / dpi) - (IndicatorBodyDip / 2.0);
|
||||
Canvas.SetLeft(indicator, indicatorLeftDip);
|
||||
Canvas.SetTop(indicator, 0);
|
||||
|
||||
rightmostPhysicalX = Math.Max(rightmostPhysicalX, b.X + b.Width);
|
||||
}
|
||||
|
||||
double paneLeftDip = (leftmostPhysicalX - overlayPhysicalOriginX) / dpi;
|
||||
double paneTopDip = (paneOriginPhysicalY - overlayPhysicalOriginY) / dpi;
|
||||
double paneWidthDip = (rightmostPhysicalX - leftmostPhysicalX) / dpi;
|
||||
|
||||
return new TaskbarPaneLayout(paneLeftDip, paneTopDip, paneWidthDip, indicatorTotalHeightDip);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="ShortcutGuide.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:ShortcutGuide.Models"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Width="586"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
IsResizable="False"
|
||||
IsShownInSwitchers="False"
|
||||
mc:Ignorable="d">
|
||||
<winuiex:WindowEx.SystemBackdrop>
|
||||
<DesktopAcrylicBackdrop />
|
||||
</winuiex:WindowEx.SystemBackdrop>
|
||||
<Page x:Name="MainPage">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="48" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Uid="TitleBar">
|
||||
<TitleBar.IconSource>
|
||||
<ImageIconSource ImageSource="/Assets/ShortcutGuide/ShortcutGuide.ico" />
|
||||
</TitleBar.IconSource>
|
||||
</TitleBar>
|
||||
|
||||
<NavigationView
|
||||
x:Name="WindowSelector"
|
||||
Grid.Row="1"
|
||||
IsBackButtonVisible="Collapsed"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
IsSettingsVisible="False"
|
||||
SelectionChanged="WindowSelector_SelectionChanged"
|
||||
Style="{StaticResource RailNavigationViewStyle}">
|
||||
<NavigationView.MenuItems />
|
||||
<NavigationView.FooterMenuItems>
|
||||
<NavigationViewItem
|
||||
x:Uid="SettingsButton"
|
||||
Icon="Setting"
|
||||
SelectsOnInvoked="False"
|
||||
Tag="Settings"
|
||||
Tapped="Settings_Tapped" />
|
||||
</NavigationView.FooterMenuItems>
|
||||
<NavigationView.Content>
|
||||
<Frame
|
||||
x:Name="ContentFrame"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1,1,0,0"
|
||||
CornerRadius="8,0,0,0" />
|
||||
</NavigationView.Content>
|
||||
<NavigationView.Resources>
|
||||
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
|
||||
</NavigationView.Resources>
|
||||
</NavigationView>
|
||||
</Grid>
|
||||
</Page>
|
||||
</winuiex:WindowEx>
|
||||
@@ -1,317 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Common.UI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using ShortcutGuide.Helpers;
|
||||
using ShortcutGuide.Models;
|
||||
using ShortcutGuide.Pages;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.System;
|
||||
using Windows.UI.WindowManagement;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
using WinUIEx.Messaging;
|
||||
|
||||
namespace ShortcutGuide
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx, IDisposable
|
||||
{
|
||||
private readonly Stopwatch _sessionStopwatch = Stopwatch.StartNew();
|
||||
private readonly Task<Dictionary<string, string?>> _getAppIdsTask;
|
||||
private Dictionary<string, string?> _currentApplicationIds = [];
|
||||
private ShortcutFile? _shortcutFile;
|
||||
private string _selectedAppName = null!;
|
||||
private string _closeType = "Unknown";
|
||||
|
||||
internal long SessionDurationMs => _sessionStopwatch.ElapsedMilliseconds;
|
||||
|
||||
internal string CloseType => _closeType;
|
||||
|
||||
private bool _setPosition;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
_getAppIdsTask = Task.Run(() =>
|
||||
{
|
||||
Program.CopyAndIndexGenerationThread.Join();
|
||||
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds(Program.ForegroundWindowHandle);
|
||||
return _currentApplicationIds;
|
||||
});
|
||||
|
||||
Title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
|
||||
#if !DEBUG
|
||||
this.SetIsAlwaysOnTop(true);
|
||||
this.SetIsShownInSwitchers(false);
|
||||
#endif
|
||||
WindowMessageMonitor msgMonitor = new(this);
|
||||
msgMonitor.WindowMessageReceived += (_, e) =>
|
||||
{
|
||||
const int WM_NCLBUTTONDBLCLK = 0x00A3;
|
||||
if (e.Message.MessageId == WM_NCLBUTTONDBLCLK)
|
||||
{
|
||||
// Disable double click on title bar to maximize window
|
||||
e.Result = 0;
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
Activated += Window_Activated;
|
||||
|
||||
Content.KeyUp += (_, e) =>
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
_closeType = "Escape";
|
||||
Close();
|
||||
}
|
||||
};
|
||||
|
||||
switch (App.ShortcutGuideProperties.Theme.Value)
|
||||
{
|
||||
case "dark":
|
||||
((FrameworkElement)Content).RequestedTheme = ElementTheme.Dark;
|
||||
this.MainPage.RequestedTheme = ElementTheme.Dark;
|
||||
break;
|
||||
case "light":
|
||||
((FrameworkElement)Content).RequestedTheme = ElementTheme.Light;
|
||||
this.MainPage.RequestedTheme = ElementTheme.Light;
|
||||
break;
|
||||
case "system":
|
||||
// Ignore, as the theme will be set by the system.
|
||||
break;
|
||||
default:
|
||||
Logger.LogError("Invalid theme value in settings: " + App.ShortcutGuideProperties.Theme.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnStateChanged(WindowState state)
|
||||
{
|
||||
if (state == WindowState.Maximized)
|
||||
{
|
||||
this.SetWindowPosition();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPositionChanged(PointInt32 position)
|
||||
{
|
||||
this.SetWindowPosition();
|
||||
}
|
||||
|
||||
private void Window_Activated(object sender, WindowActivatedEventArgs e)
|
||||
{
|
||||
if (e.WindowActivationState == WindowActivationState.Deactivated && !this._taskBarWindowActivated)
|
||||
{
|
||||
#if !DEBUG
|
||||
_closeType = "Deactivated";
|
||||
Close();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (this._taskBarWindowActivated)
|
||||
{
|
||||
this._taskBarWindowActivated = false;
|
||||
this.BringToFront();
|
||||
}
|
||||
|
||||
// The code below sets the position of the window to the center of the monitor, but only if it hasn't been set before.
|
||||
if (!this._setPosition)
|
||||
{
|
||||
this.SetWindowPosition();
|
||||
this._setPosition = true;
|
||||
|
||||
AppWindow.Changed += (_, a) =>
|
||||
{
|
||||
if (!a.DidPresenterChange)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.SetWindowPosition();
|
||||
};
|
||||
}
|
||||
|
||||
_ = this.InitializeNavItemsAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeNavItemsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_currentApplicationIds = await _getAppIdsTask.ConfigureAwait(true);
|
||||
this.SetNavItems();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to initialize navigation items.", ex);
|
||||
_closeType = "InitializationFailed";
|
||||
this.DispatcherQueue.TryEnqueue(() => this.Close());
|
||||
}
|
||||
}
|
||||
|
||||
private void SetNavItems()
|
||||
{
|
||||
// Populate the window selector with the current application IDs if it is empty.
|
||||
// TO DO: Check if Settings button is considered an item too.
|
||||
if (this.WindowSelector.MenuItems.Count == 0)
|
||||
{
|
||||
string defaultShellName = ManifestInterpreter.GetCachedIndexYamlFile().DefaultShellName;
|
||||
|
||||
foreach (var (item, executablePath) in this._currentApplicationIds)
|
||||
{
|
||||
if (item == defaultShellName)
|
||||
{
|
||||
var pathData = (string)Application.Current.Resources["WindowsLogoPathData"];
|
||||
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = "Windows", Icon = CreatePathIcon(pathData) });
|
||||
}
|
||||
else if (item == "Microsoft.PowerToys")
|
||||
{
|
||||
var pathData = (string)Application.Current.Resources["PowerToysLogoPathData"];
|
||||
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = CreatePathIcon(pathData) });
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
IconElement icon = BuildNavIcon(executablePath);
|
||||
this.WindowSelector.MenuItems.Add(new NavigationViewItem { Name = item, Content = ManifestInterpreter.GetShortcutsOfApplication(item).Name, Icon = icon });
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogError($"Failed to build nav item for application '{item}' (executable '{executablePath}').", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.WindowSelector.MenuItems.Count > 0)
|
||||
{
|
||||
this.WindowSelector.SelectedItem = this.WindowSelector.MenuItems[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IconElement BuildNavIcon(string? executablePath)
|
||||
{
|
||||
BitmapImage? bitmap = IconHelper.TryGetExecutableIcon(executablePath);
|
||||
if (bitmap is not null)
|
||||
{
|
||||
return new ImageIcon { Source = bitmap };
|
||||
}
|
||||
|
||||
return new FontIcon { Glyph = "\uEB91" };
|
||||
}
|
||||
|
||||
private static PathIcon CreatePathIcon(string pathData)
|
||||
{
|
||||
var geometry = (Geometry)XamlBindingHelper.ConvertValue(typeof(Geometry), pathData);
|
||||
return new PathIcon
|
||||
{
|
||||
Data = geometry,
|
||||
Width = 20,
|
||||
Height = 20,
|
||||
};
|
||||
}
|
||||
|
||||
private bool _hasMovedToRightMonitor;
|
||||
|
||||
private void SetWindowPosition()
|
||||
{
|
||||
if (!this._hasMovedToRightMonitor)
|
||||
{
|
||||
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
|
||||
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
|
||||
this._hasMovedToRightMonitor = true;
|
||||
}
|
||||
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
|
||||
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
|
||||
|
||||
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
|
||||
var taskbarWindow = App.TaskBarWindow.AppWindow;
|
||||
bool taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
|
||||
bool taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
|
||||
|
||||
double newHeight = monitorRect.Height / dpi;
|
||||
if (taskbarOnLeft || taskbarOnRight)
|
||||
{
|
||||
newHeight -= taskbarWindow.Size.Height;
|
||||
}
|
||||
|
||||
MaxHeight = newHeight;
|
||||
MinHeight = newHeight;
|
||||
Height = newHeight;
|
||||
|
||||
int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
|
||||
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
|
||||
: (int)monitorRect.X;
|
||||
|
||||
this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks whether the taskbar window was activated. So that the main window does not close.
|
||||
/// </summary>
|
||||
private bool _taskBarWindowActivated;
|
||||
|
||||
private void WindowSelector_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.SelectedItem is not NavigationViewItem selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectedAppName = selectedItem.Name;
|
||||
App.CurrentAppName = this._selectedAppName;
|
||||
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
|
||||
|
||||
App.TaskBarWindow.Hide();
|
||||
if (this._shortcutFile is ShortcutFile file)
|
||||
{
|
||||
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
|
||||
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
|
||||
{
|
||||
this._taskBarWindowActivated = true;
|
||||
App.TaskBarWindow.Activate();
|
||||
}
|
||||
|
||||
// Reposition before navigating so the taskbar window does not clip into the main window.
|
||||
this.SetWindowPosition();
|
||||
this.ContentFrame.Navigate(
|
||||
typeof(ShortcutsPage),
|
||||
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
|
||||
}
|
||||
}
|
||||
|
||||
private void Settings_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ShortcutGuide);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_getAppIdsTask.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="ShortcutGuide.OverlayWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:ShortcutGuide.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
IsResizable="False"
|
||||
IsShownInSwitchers="False"
|
||||
IsTitleBarVisible="False"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<!--
|
||||
Full-monitor transparent host. The system backdrop is set in code-
|
||||
behind to TransparentTintBackdrop so the area outside the pseudo-
|
||||
windows is fully see-through; per-card acrylic is supplied inside
|
||||
each UserControl via SystemBackdropElement + AlwaysActiveDesktopAcrylicBackdrop.
|
||||
-->
|
||||
|
||||
<!--
|
||||
OverlayRoot must have a Background brush so it is hit-test opaque —
|
||||
otherwise clicks on the transparent margin fall through to whatever
|
||||
is underneath and the click-outside-to-close UX breaks. A fully
|
||||
transparent brush is enough; the visual transparency comes from the
|
||||
system backdrop above.
|
||||
-->
|
||||
<Grid
|
||||
x:Name="OverlayRoot"
|
||||
Background="Transparent"
|
||||
PointerPressed="OverlayRoot_PointerPressed">
|
||||
<controls:MainPaneControl
|
||||
x:Name="MainPane"
|
||||
Width="586"
|
||||
Margin="16"
|
||||
Horizo ntalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<!--
|
||||
TaskbarPane sits in an absolute layer (Canvas) because it must
|
||||
align pixel-perfectly with the underlying taskbar buttons. The
|
||||
overlay positions it via Canvas.SetLeft/Top once UpdateLayout
|
||||
returns the desired rect.
|
||||
-->
|
||||
<Canvas x:Name="TaskbarLayer" IsHitTestVisible="True">
|
||||
<controls:TaskbarPaneControl x:Name="TaskbarPane" Visibility="Collapsed" />
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
@@ -0,0 +1,540 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Common.UI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using ShortcutGuide.Controls;
|
||||
using ShortcutGuide.Helpers;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
using WinUIEx.Messaging;
|
||||
|
||||
namespace ShortcutGuide
|
||||
{
|
||||
/// <summary>
|
||||
/// Single transparent host that replaces both the previous
|
||||
/// <c>MainWindow</c> and <c>TaskbarWindow</c>. It covers the work area
|
||||
/// of the cursor's monitor and renders the two surfaces as pseudo-windows
|
||||
/// (<see cref="MainPaneControl"/> + <see cref="TaskbarPaneControl"/>)
|
||||
/// inside its XAML tree, so they can be coordinated by a single
|
||||
/// dispatcher, activate together, and (later) share an animation
|
||||
/// timeline.
|
||||
/// </summary>
|
||||
public sealed partial class OverlayWindow : WindowEx
|
||||
{
|
||||
// DWM attributes used to disable the Win11 1-pixel system border and
|
||||
// forced corner rounding. Without these the OS draws a faint stroke
|
||||
// (and a small drop-shadow) around the transparent overlay, which
|
||||
// looks like a phantom rectangle on top of the desktop.
|
||||
private const uint DwmwaColorNone = 0xFFFFFFFE;
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwaBorderColor = 34;
|
||||
private const int DwmwcpDoNotRound = 1;
|
||||
|
||||
private readonly Stopwatch _sessionStopwatch = Stopwatch.StartNew();
|
||||
private string _closeType = "Unknown";
|
||||
private bool _isClosing;
|
||||
|
||||
// Set true around AppWindow.MoveAndResize() so the WndProc swallows
|
||||
// WM_DPICHANGED. Without this, WinUIEx re-scales the rect we just
|
||||
// wrote by (newDpi / oldDpi), producing a 1.5x/0.66x window on
|
||||
// cross-monitor moves between mixed-DPI displays.
|
||||
private bool _suppressDpiChange;
|
||||
|
||||
internal long SessionDurationMs => _sessionStopwatch.ElapsedMilliseconds;
|
||||
|
||||
internal string CloseType => _closeType;
|
||||
|
||||
public MainPaneControl MainPaneControl => this.MainPane;
|
||||
|
||||
internal TaskbarPaneControl TaskbarPaneControl => this.TaskbarPane;
|
||||
|
||||
public OverlayWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
// TransparentTintBackdrop cannot be referenced from XAML in the
|
||||
// current Windows App SDK schema; set it here instead.
|
||||
this.SystemBackdrop = new TransparentTintBackdrop();
|
||||
|
||||
this.Title = ResourceLoaderInstance.ResourceLoader.GetString("Title");
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
|
||||
// Install the message hook BEFORE the first MoveAndResize so the
|
||||
// WM_DPICHANGED suppression is in place from the very first
|
||||
// cross-monitor move. Otherwise the constructor's initial
|
||||
// RepositionToCursorMonitor() lets the default WndProc auto-
|
||||
// resize the window by (newDpi/oldDpi) and we get a 1.5×
|
||||
// overlay on the laptop screen. Also disable
|
||||
// WM_NCLBUTTONDBLCLK so the OS doesn't try to maximize when
|
||||
// the user double-clicks anywhere in the (collapsed) caption
|
||||
// area. Matches the original MainWindow behavior.
|
||||
WindowMessageMonitor msgMonitor = new(this);
|
||||
msgMonitor.WindowMessageReceived += (_, e) =>
|
||||
{
|
||||
const int WM_NCLBUTTONDBLCLK = 0x00A3;
|
||||
const int WM_DPICHANGED = 0x02E0;
|
||||
if (e.Message.MessageId == WM_NCLBUTTONDBLCLK)
|
||||
{
|
||||
e.Result = 0;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Message.MessageId == WM_DPICHANGED && _suppressDpiChange)
|
||||
{
|
||||
// We've already written the correctly-scaled physical
|
||||
// rect for the target monitor — let WinUI update its
|
||||
// RasterizationScale, but DO NOT let the default
|
||||
// WndProc resize the window to its suggested rect
|
||||
// (which is the OLD size scaled by newDpi/oldDpi and
|
||||
// is what produced the doubled overlay).
|
||||
e.Result = 0;
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
StripNativeChrome();
|
||||
|
||||
// Pre-size and position BEFORE Activate so the first frame the
|
||||
// user sees is already at the correct work-area size on the
|
||||
// cursor's monitor. Doing this in OnActivated produces a single
|
||||
// wrong-sized flash that "snaps" to the right size when focus
|
||||
// changes (because a later layout pass picks up the correct
|
||||
// AppWindow.Size).
|
||||
RepositionToCursorMonitor();
|
||||
ApplyMainPaneAlignment();
|
||||
|
||||
#if !DEBUG
|
||||
this.SetIsAlwaysOnTop(true);
|
||||
this.SetIsShownInSwitchers(false);
|
||||
#endif
|
||||
|
||||
this.Activated += OnActivated;
|
||||
|
||||
// Esc closes the overlay regardless of which pseudo-window has
|
||||
// keyboard focus (handled at the Window.Content root because the
|
||||
// event bubbles up from whichever inner element has focus).
|
||||
if (this.Content is UIElement contentRoot)
|
||||
{
|
||||
contentRoot.KeyUp += OnContentKeyUp;
|
||||
}
|
||||
|
||||
ApplyThemeFromSettings();
|
||||
|
||||
// Reposition when the window's presenter changes (e.g. from a
|
||||
// PresenterKind switch). Matches the original MainWindow behavior.
|
||||
this.AppWindow.Changed += (_, args) =>
|
||||
{
|
||||
if (!args.DidPresenterChange)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RepositionToCursorMonitor();
|
||||
};
|
||||
|
||||
this.MainPane.SelectedAppTaskbarVisibilityChanged += OnMainPaneTaskbarVisibilityChanged;
|
||||
this.MainPane.InitializationFailed += OnMainPaneInitializationFailed;
|
||||
this.MainPane.CloseRequested += (_, _) =>
|
||||
{
|
||||
_closeType = "CloseButton";
|
||||
CloseAnimated();
|
||||
};
|
||||
|
||||
// Reveal the main pane after the window has loaded so the
|
||||
// Implicit.ShowAnimations play on first appearance.
|
||||
this.OverlayRoot.Loaded += (_, _) =>
|
||||
{
|
||||
this.MainPane.Visibility = Visibility.Visible;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips the native window frame (WS_CAPTION/WS_THICKFRAME), the
|
||||
/// extended edge styles (WS_EX_WINDOWEDGE/CLIENTEDGE/DLGMODALFRAME)
|
||||
/// and disables the Win11 DWM 1-px system border + forced corner
|
||||
/// rounding. Also marks the window as a tool window
|
||||
/// (WS_EX_TOOLWINDOW) so the OS doesn't draw the default toplevel
|
||||
/// chrome that produces the faint white outline on transparent
|
||||
/// surfaces. Idempotent and safe to call again after the window has
|
||||
/// been moved across monitors — a cross-monitor DPI change can reset
|
||||
/// some of these attributes.
|
||||
/// </summary>
|
||||
private void StripNativeChrome()
|
||||
{
|
||||
nint hwnd = WindowNative.GetWindowHandle(this);
|
||||
HwndExtensions.ToggleWindowStyle(hwnd, false, WindowStyle.TiledWindow);
|
||||
|
||||
// Extended styles: drop the 3-D-ish window edges Windows draws for
|
||||
// ordinary top-level windows and switch the window into the
|
||||
// "tool window" category. WS_EX_TOOLWINDOW also suppresses the
|
||||
// small caption Windows would otherwise reserve, which removes
|
||||
// the last visible 1-px line around a transparent overlay.
|
||||
int exStyle = NativeMethods.GetWindowLongW(hwnd, NativeMethods.GWL_EXSTYLE);
|
||||
int newExStyle = (exStyle
|
||||
& ~NativeMethods.WS_EX_WINDOWEDGE
|
||||
& ~NativeMethods.WS_EX_CLIENTEDGE
|
||||
& ~NativeMethods.WS_EX_DLGMODALFRAME)
|
||||
| NativeMethods.WS_EX_TOOLWINDOW;
|
||||
if (newExStyle != exStyle)
|
||||
{
|
||||
_ = NativeMethods.SetWindowLongW(hwnd, NativeMethods.GWL_EXSTYLE, newExStyle);
|
||||
}
|
||||
|
||||
// SWP_FRAMECHANGED forces DWM to re-evaluate the frame after
|
||||
// style changes and after DwmExtendFrameIntoClientArea below.
|
||||
const uint SWP_NOMOVE = 0x0002;
|
||||
const uint SWP_NOSIZE = 0x0001;
|
||||
const uint SWP_NOZORDER = 0x0004;
|
||||
const uint SWP_NOACTIVATE = 0x0010;
|
||||
const uint SWP_FRAMECHANGED = 0x0020;
|
||||
|
||||
uint borderColor = DwmwaColorNone;
|
||||
_ = DwmSetWindowAttribute(hwnd, DwmwaBorderColor, ref borderColor, sizeof(uint));
|
||||
|
||||
int cornerPref = DwmwcpDoNotRound;
|
||||
_ = DwmSetWindowAttribute(hwnd, DwmwaWindowCornerPreference, ref cornerPref, sizeof(int));
|
||||
|
||||
// Disable non-client rendering entirely so the DWM doesn't draw
|
||||
// ANY frame/border chrome (not even a 1-px line).
|
||||
int ncrpDisabled = 2; // DWMNCRP_DISABLED
|
||||
_ = DwmSetWindowAttribute(hwnd, 2 /* DWMWA_NCRENDERING_POLICY */, ref ncrpDisabled, sizeof(int));
|
||||
|
||||
// Extend the frame into the entire client area. With a transparent
|
||||
// backdrop this eliminates the last possible seam between the
|
||||
// non-client and client regions that the DWM might draw.
|
||||
var margins = new MARGINS { Left = -1, Right = -1, Top = -1, Bottom = -1 };
|
||||
_ = DwmExtendFrameIntoClientArea(hwnd, ref margins);
|
||||
|
||||
_ = NativeMethods.SetWindowPos(
|
||||
hwnd,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||||
}
|
||||
|
||||
private void ApplyThemeFromSettings()
|
||||
{
|
||||
switch (App.ShortcutGuideProperties.Theme.Value)
|
||||
{
|
||||
case "dark":
|
||||
if (this.Content is FrameworkElement darkRoot)
|
||||
{
|
||||
darkRoot.RequestedTheme = ElementTheme.Dark;
|
||||
}
|
||||
|
||||
break;
|
||||
case "light":
|
||||
if (this.Content is FrameworkElement lightRoot)
|
||||
{
|
||||
lightRoot.RequestedTheme = ElementTheme.Light;
|
||||
}
|
||||
|
||||
break;
|
||||
case "system":
|
||||
// Default — follow the system theme.
|
||||
break;
|
||||
default:
|
||||
Logger.LogError("Invalid theme value in settings: " + App.ShortcutGuideProperties.Theme.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActivated(object sender, WindowActivatedEventArgs e)
|
||||
{
|
||||
if (e.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
#if !DEBUG
|
||||
_closeType = "Deactivated";
|
||||
CloseAnimated();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnContentKeyUp(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
_closeType = "Escape";
|
||||
CloseAnimated();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the overlay when the user clicks outside the pseudo-windows
|
||||
/// (i.e. anywhere in the transparent area). Clicks on
|
||||
/// <see cref="MainPaneControl"/> or <see cref="TaskbarPaneControl"/>
|
||||
/// are ignored — we detect them by walking the visual tree from
|
||||
/// <see cref="RoutedEventArgs.OriginalSource"/>.
|
||||
/// </summary>
|
||||
private void OverlayRoot_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is DependencyObject src && IsInsidePseudoWindow(src))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_closeType = "ClickOutside";
|
||||
CloseAnimated();
|
||||
}
|
||||
|
||||
private bool IsInsidePseudoWindow(DependencyObject src)
|
||||
{
|
||||
DependencyObject? current = src;
|
||||
while (current is not null)
|
||||
{
|
||||
if (ReferenceEquals(current, this.MainPane) || ReferenceEquals(current, this.TaskbarPane))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnMainPaneTaskbarVisibilityChanged(object? sender, bool shouldShow)
|
||||
{
|
||||
if (!shouldShow)
|
||||
{
|
||||
this.TaskbarPane.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateTaskbarPaneLayout();
|
||||
}
|
||||
|
||||
private void OnMainPaneInitializationFailed(object? sender, EventArgs e)
|
||||
{
|
||||
_closeType = "InitializationFailed";
|
||||
this.DispatcherQueue.TryEnqueue(() => this.Close());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers hide animations on both pseudo-windows, waits for the
|
||||
/// longest animation to finish, then closes the window. Multiple
|
||||
/// calls are coalesced via <see cref="_isClosing"/>.
|
||||
/// </summary>
|
||||
public void CloseAnimated()
|
||||
{
|
||||
if (_isClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isClosing = true;
|
||||
|
||||
// Collapse both pseudo-windows so their Implicit.HideAnimations play
|
||||
this.MainPane.Visibility = Visibility.Collapsed;
|
||||
this.TaskbarPane.Visibility = Visibility.Collapsed;
|
||||
|
||||
var timer = this.DispatcherQueue.CreateTimer();
|
||||
timer.Interval = TimeSpan.FromMilliseconds(217);
|
||||
timer.IsRepeating = false;
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
_isClosing = false;
|
||||
|
||||
MainPane.Hide();
|
||||
|
||||
this.AppWindow.Hide();
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes the taskbar pane's indicator children and applies the
|
||||
/// resulting layout to the Canvas-positioned pseudo-window.
|
||||
/// </summary>
|
||||
public void UpdateTaskbarPaneLayout()
|
||||
{
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
|
||||
Rect workArea = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
|
||||
|
||||
var layout = this.TaskbarPane.UpdateTasklistButtons(
|
||||
overlayPhysicalOriginX: this.AppWindow.Position.X,
|
||||
overlayPhysicalOriginY: this.AppWindow.Position.Y,
|
||||
dpi: dpi,
|
||||
workAreaBottomPhysical: workArea.Bottom);
|
||||
|
||||
if (layout is null)
|
||||
{
|
||||
this.TaskbarPane.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
this.TaskbarPane.Width = layout.Value.Width;
|
||||
this.TaskbarPane.Height = layout.Value.Height;
|
||||
Canvas.SetLeft(this.TaskbarPane, layout.Value.Left);
|
||||
Canvas.SetTop(this.TaskbarPane, layout.Value.Top);
|
||||
this.TaskbarPane.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sizes and positions the overlay to cover the work area of the
|
||||
/// monitor that currently contains the cursor. Looks the monitor up
|
||||
/// directly from the cursor position so the work-area rect doesn't
|
||||
/// depend on a previous asynchronous <see cref="AppWindow.Move"/>
|
||||
/// having actually committed yet.
|
||||
/// </summary>
|
||||
private void RepositionToCursorMonitor()
|
||||
{
|
||||
if (!NativeMethods.GetCursorPos(out NativeMethods.POINT cursor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IntPtr hmon = NativeMethods.MonitorFromPoint(
|
||||
cursor,
|
||||
(int)NativeMethods.MonitorFromWindowDwFlags.MONITOR_DEFAULTTONEAREST);
|
||||
if (hmon == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mi = new NativeMethods.MONITORINFO
|
||||
{
|
||||
CbSize = (uint)Marshal.SizeOf<NativeMethods.MONITORINFO>(),
|
||||
};
|
||||
if (!NativeMethods.GetMonitorInfoW(hmon, ref mi))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// rcWork is in physical pixels (virtual-screen coordinates).
|
||||
// AppWindow.MoveAndResize takes physical pixels too — no DPI math
|
||||
// required. Using AppWindow directly avoids the WinUIEx
|
||||
// WindowEx.MoveAndResize extension whose x/y are physical but
|
||||
// whose w/h are DIPs (and get DPI-scaled internally).
|
||||
var rect = new Windows.Graphics.RectInt32(
|
||||
mi.RcWork.Left,
|
||||
mi.RcWork.Top,
|
||||
mi.RcWork.Right - mi.RcWork.Left,
|
||||
mi.RcWork.Bottom - mi.RcWork.Top);
|
||||
|
||||
// Cross-monitor moves trigger WM_DPICHANGED which causes WinUIEx
|
||||
// to auto-rescale the window by (newDpi/oldDpi) ON TOP of the
|
||||
// physical rect we just provided — making the overlay 1.5×
|
||||
// (or 0.66×) the work area on mixed-DPI multi-monitor setups.
|
||||
// CmdPal's MoveAndResizeDpiAware pattern: zero out MinWidth/
|
||||
// MinHeight (WinUIEx uses current DPI to recompute them in
|
||||
// physical px), set _suppressDpiChange so the WndProc swallows
|
||||
// WM_DPICHANGED, then MoveAndResize.
|
||||
var origMinWidth = this.MinWidth;
|
||||
var origMinHeight = this.MinHeight;
|
||||
_suppressDpiChange = true;
|
||||
try
|
||||
{
|
||||
this.MinWidth = 0;
|
||||
this.MinHeight = 0;
|
||||
this.AppWindow.MoveAndResize(rect);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.MinWidth = origMinWidth;
|
||||
this.MinHeight = origMinHeight;
|
||||
_suppressDpiChange = false;
|
||||
}
|
||||
|
||||
// Cross-monitor moves can trigger WM_DPICHANGED, and Windows may
|
||||
// reset some of our DWM attributes (border color, corner pref)
|
||||
// during that transition. Re-apply them defensively so the
|
||||
// overlay never reveals an OS-drawn 1-px stroke or rounded
|
||||
// shadow.
|
||||
StripNativeChrome();
|
||||
|
||||
// The taskbar pane is anchored against the bottom of the work area,
|
||||
// so any move/resize needs a fresh layout pass.
|
||||
if (this.TaskbarPane.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateTaskbarPaneLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the left/right alignment from the user setting to the
|
||||
/// main pseudo-window.
|
||||
/// </summary>
|
||||
private void ApplyMainPaneAlignment()
|
||||
{
|
||||
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
|
||||
bool isRight = windowPosition == ShortcutGuideWindowPosition.Right;
|
||||
|
||||
this.MainPane.HorizontalAlignment = isRight
|
||||
? HorizontalAlignment.Right
|
||||
: HorizontalAlignment.Left;
|
||||
|
||||
// Slide direction matches the pane's edge: left-aligned slides
|
||||
// from the left, right-aligned slides from the right — same as
|
||||
// Windows 11 Widgets (left) and Action Center (right).
|
||||
string slideFrom = isRight ? "20,0,0" : "-20,0,0";
|
||||
|
||||
var showAnimations = new ImplicitAnimationSet();
|
||||
showAnimations.Add(new OpacityAnimation { From = 0, To = 1.0, Duration = TimeSpan.FromMilliseconds(367) });
|
||||
showAnimations.Add(new TranslationAnimation
|
||||
{
|
||||
From = slideFrom,
|
||||
To = "0,0,0",
|
||||
Duration = TimeSpan.FromMilliseconds(367),
|
||||
EasingType = EasingType.Cubic,
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
});
|
||||
Implicit.SetShowAnimations(this.MainPane, showAnimations);
|
||||
|
||||
var hideAnimations = new ImplicitAnimationSet();
|
||||
hideAnimations.Add(new OpacityAnimation { From = 1.0, To = 0, Duration = TimeSpan.FromMilliseconds(200) });
|
||||
hideAnimations.Add(new TranslationAnimation
|
||||
{
|
||||
From = "0,0,0",
|
||||
To = slideFrom,
|
||||
Duration = TimeSpan.FromMilliseconds(200),
|
||||
EasingType = EasingType.Cubic,
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
});
|
||||
Implicit.SetHideAnimations(this.MainPane, hideAnimations);
|
||||
}
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, ref uint pvAttribute, int cbAttribute);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, ref int pvAttribute, int cbAttribute);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static partial int DwmExtendFrameIntoClientArea(nint hwnd, ref MARGINS pMarInset);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MARGINS
|
||||
{
|
||||
public int Left;
|
||||
public int Right;
|
||||
public int Top;
|
||||
public int Bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,9 @@
|
||||
</Page.Resources>
|
||||
<ScrollViewer>
|
||||
<ItemsRepeater
|
||||
x:Name="MainItemsRepeater"
|
||||
Margin="0,0,0,24"
|
||||
ElementClearing="MainItemsRepeater_ElementClearing"
|
||||
ItemTemplate="{StaticResource RowTemplateSelector}"
|
||||
ItemsSource="{x:Bind Rows, Mode=OneWay}" />
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -6,34 +6,50 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using ShortcutGuide.Controls;
|
||||
using ShortcutGuide.Helpers;
|
||||
using ShortcutGuide.Models;
|
||||
using ShortcutGuide.ViewModels;
|
||||
|
||||
namespace ShortcutGuide.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays every category of shortcuts for the selected application as a single,
|
||||
/// flat virtualized list (Pinned, Recommended, each category, Taskbar).
|
||||
/// The list is flat — header / subtitle / shortcut / empty-placeholder rows share one
|
||||
/// ItemsRepeater — so virtualization realizes only rows in the viewport instead of
|
||||
/// every row of every realized section.
|
||||
/// </summary>
|
||||
public sealed partial class ShortcutsPage : Page
|
||||
{
|
||||
private const string TaskbarSectionMarker = "<TASKBAR1-9>";
|
||||
|
||||
private ShortcutFile? _shortcutFile;
|
||||
private string _appName = string.Empty;
|
||||
private bool _isEventSubscribed;
|
||||
|
||||
public ObservableCollection<ShortcutListItem> Rows { get; } = new();
|
||||
|
||||
public ShortcutsPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.Unloaded += (_, _) => PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
|
||||
|
||||
this.Unloaded += (_, _) =>
|
||||
{
|
||||
UnsubscribeFromEvents();
|
||||
ClearData();
|
||||
ForceItemsRepeaterCleanup();
|
||||
};
|
||||
}
|
||||
|
||||
private void MainItemsRepeater_ElementClearing(ItemsRepeater sender, ItemsRepeaterElementClearingEventArgs args)
|
||||
{
|
||||
// Aggressively clean up elements as they're being cleared
|
||||
if (args.Element is FrameworkElement element)
|
||||
{
|
||||
// Clear DataContext to break binding references
|
||||
element.DataContext = null;
|
||||
if (element is ShortcutItemView shortcutView)
|
||||
{
|
||||
shortcutView.ClearValue(ShortcutItemView.ShortcutProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
@@ -45,13 +61,73 @@ namespace ShortcutGuide.Pages
|
||||
this.RebuildRows();
|
||||
}
|
||||
|
||||
PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
|
||||
UnsubscribeFromEvents();
|
||||
PinnedShortcutsHelper.PinnedShortcutsChanged += this.OnPinnedShortcutsChanged;
|
||||
_isEventSubscribed = true;
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
|
||||
UnsubscribeFromEvents();
|
||||
ClearData();
|
||||
ForceItemsRepeaterCleanup();
|
||||
}
|
||||
|
||||
public void ClearData()
|
||||
{
|
||||
// Clear the collection to trigger ElementClearing for all items
|
||||
this.Rows.Clear();
|
||||
_shortcutFile = null;
|
||||
_appName = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces ItemsRepeater to release all cached/recycled elements and clear WinUI's internal template cache.
|
||||
/// This addresses the ConcurrentDictionary WeakReference cache leak in WinUI.
|
||||
/// </summary>
|
||||
private void ForceItemsRepeaterCleanup()
|
||||
{
|
||||
if (this.MainItemsRepeater == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get the parent ScrollViewer
|
||||
if (this.Content is ScrollViewer scrollViewer)
|
||||
{
|
||||
// Create a brand new ItemsRepeater
|
||||
var newRepeater = new ItemsRepeater
|
||||
{
|
||||
Name = "MainItemsRepeater",
|
||||
Margin = new Thickness(0, 0, 0, 24),
|
||||
ItemTemplate = this.Resources["RowTemplateSelector"] as IElementFactory,
|
||||
Layout = new StackLayout(),
|
||||
};
|
||||
|
||||
newRepeater.ElementClearing += MainItemsRepeater_ElementClearing;
|
||||
|
||||
scrollViewer.Content = newRepeater;
|
||||
}
|
||||
|
||||
GC.Collect(2, GCCollectionMode.Aggressive, true, true);
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect(2, GCCollectionMode.Aggressive, true, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail silently
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromEvents()
|
||||
{
|
||||
if (_isEventSubscribed)
|
||||
{
|
||||
PinnedShortcutsHelper.PinnedShortcutsChanged -= this.OnPinnedShortcutsChanged;
|
||||
_isEventSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildRows()
|
||||
@@ -105,8 +181,6 @@ namespace ShortcutGuide.Pages
|
||||
{
|
||||
string name = category.SectionName ?? string.Empty;
|
||||
|
||||
// Taskbar marker may carry trailing text in the manifest (e.g. "<TASKBAR1-9>Taskbar Shortcuts");
|
||||
// detect it by prefix and hand it off to the dedicated Taskbar section below.
|
||||
if (name.StartsWith(TaskbarSectionMarker, StringComparison.Ordinal))
|
||||
{
|
||||
taskbarCategory = category;
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="ShortcutGuide.ShortcutGuideXAML.TaskbarWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:ShortcutGuide"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="TaskbarWindow"
|
||||
Width="600"
|
||||
Height="200"
|
||||
IsAlwaysOnTop="True"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
IsResizable="False"
|
||||
IsShownInSwitchers="False"
|
||||
IsTitleBarVisible="False"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<DesktopAcrylicBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="WindowsLogoColumnWidth" Width="68" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Canvas x:Name="KeyHolder" Grid.Column="1" />
|
||||
<StackPanel
|
||||
Margin="8"
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Border
|
||||
Width="34"
|
||||
Height="34"
|
||||
Padding="8"
|
||||
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}">
|
||||
<Viewbox>
|
||||
<PathIcon Data="M9 20H0V11H9V20ZM20 20H11V11H20V20ZM9 9H0V0H9V9ZM20 9H11V0H20V9Z" />
|
||||
</Viewbox>
|
||||
</Border>
|
||||
<TextBlock
|
||||
Margin="0,-3,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="+" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using ShortcutGuide.Controls;
|
||||
using ShortcutGuide.Helpers;
|
||||
using Windows.Foundation;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
using static ShortcutGuide.NativeMethods;
|
||||
|
||||
namespace ShortcutGuide.ShortcutGuideXAML
|
||||
{
|
||||
public sealed partial class TaskbarWindow : WindowEx
|
||||
{
|
||||
private float DPI => DpiHelper.GetDPIScaleForWindow(WindowNative.GetWindowHandle(this));
|
||||
|
||||
private Rect WorkArea => DisplayHelper.GetWorkAreaForDisplayWithWindow(WindowNative.GetWindowHandle(this));
|
||||
|
||||
public TaskbarWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.UpdateTasklistButtons();
|
||||
this.Activated += (_, _) => this.UpdateTasklistButtons();
|
||||
}
|
||||
|
||||
public void UpdateTasklistButtons()
|
||||
{
|
||||
// This move ensures the window spawns on the same monitor as the main window
|
||||
AppWindow.MoveInZOrderAtBottom();
|
||||
AppWindow.Move(App.MainWindow.AppWindow.Position);
|
||||
TasklistButton[] buttons = [];
|
||||
try
|
||||
{
|
||||
buttons = TasklistPositions.GetButtons();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
|
||||
}
|
||||
|
||||
if (buttons.Length == 0)
|
||||
{
|
||||
AppWindow.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
float dpi = this.DPI;
|
||||
double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value;
|
||||
double windowHeight = 58;
|
||||
double windowMargin = 8 * dpi;
|
||||
double windowWidth = windowsLogoColumnWidth;
|
||||
double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi);
|
||||
double yPosition = this.WorkArea.Bottom - (windowHeight * dpi);
|
||||
|
||||
this.KeyHolder.Children.Clear();
|
||||
|
||||
foreach (TasklistButton b in buttons)
|
||||
{
|
||||
TaskbarIndicator indicator = new()
|
||||
{
|
||||
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
|
||||
Height = b.Height / dpi,
|
||||
Width = b.Width / dpi,
|
||||
};
|
||||
|
||||
windowWidth += indicator.Width;
|
||||
|
||||
this.KeyHolder.Children.Add(indicator);
|
||||
|
||||
double indicatorPos = (b.X - xPosition) / dpi;
|
||||
Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth);
|
||||
}
|
||||
|
||||
this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight);
|
||||
AppWindow.MoveInZOrderAtTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,9 @@
|
||||
<data name="CloseButton.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
</data>
|
||||
<data name="CloseButton.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
</data>
|
||||
<data name="ErrorInAppParsing" xml:space="preserve">
|
||||
<value>Error displaying the application's shortcuts</value>
|
||||
<comment>Shortcuts refers to keyboard shortcuts; Application refers to a PC application</comment>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "../interface/powertoy_module_interface.h"
|
||||
#include "Generated Files/resource.h"
|
||||
#include <common/SettingsAPI/settings_objects.h>
|
||||
#include <common/utils/EventWaiter.h>
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD /*ul_reason_for_call*/, LPVOID /*lpReserved*/)
|
||||
{
|
||||
@@ -37,9 +36,10 @@ public:
|
||||
}
|
||||
|
||||
triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT);
|
||||
triggerEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](DWORD) {
|
||||
OnHotkeyEx();
|
||||
});
|
||||
if (!triggerEvent)
|
||||
{
|
||||
Logger::warn(L"Failed to create {} event. {}", CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
InitSettings();
|
||||
}
|
||||
@@ -91,6 +91,7 @@ public:
|
||||
if (!_enabled)
|
||||
{
|
||||
_enabled = true;
|
||||
StartProcess();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -127,6 +128,10 @@ public:
|
||||
{
|
||||
CloseHandle(exitEvent);
|
||||
}
|
||||
if (triggerEvent)
|
||||
{
|
||||
CloseHandle(triggerEvent);
|
||||
}
|
||||
|
||||
delete this;
|
||||
}
|
||||
@@ -145,19 +150,12 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsProcessActive())
|
||||
if (!IsProcessActive())
|
||||
{
|
||||
TerminateProcess(m_hProcess, 0);
|
||||
return;
|
||||
StartProcess();
|
||||
}
|
||||
|
||||
if (m_hProcess)
|
||||
{
|
||||
CloseHandle(m_hProcess);
|
||||
m_hProcess = nullptr;
|
||||
}
|
||||
|
||||
StartProcess();
|
||||
SetEvent(triggerEvent);
|
||||
}
|
||||
|
||||
virtual void send_settings_telemetry() override
|
||||
@@ -168,6 +166,8 @@ public:
|
||||
Logger::error("Failed to create a process to send settings telemetry");
|
||||
}
|
||||
}
|
||||
virtual bool keep_track_of_pressed_win_key() override { return true; }
|
||||
virtual UINT milliseconds_win_key_must_be_pressed() override { return 900; }
|
||||
|
||||
private:
|
||||
std::wstring app_name;
|
||||
@@ -187,7 +187,6 @@ private:
|
||||
|
||||
HANDLE triggerEvent;
|
||||
HANDLE exitEvent;
|
||||
EventWaiter triggerEventWaiter;
|
||||
|
||||
bool StartProcess(std::wstring args = L"")
|
||||
{
|
||||
@@ -196,6 +195,11 @@ private:
|
||||
ResetEvent(exitEvent);
|
||||
}
|
||||
|
||||
if (triggerEvent)
|
||||
{
|
||||
ResetEvent(triggerEvent);
|
||||
}
|
||||
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
std::wstring executable_args = L"";
|
||||
executable_args.append(std::to_wstring(powertoys_pid));
|
||||
@@ -310,6 +314,14 @@ private:
|
||||
m_hotkey.vkCode = VK_OEM_2;
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsKeyPressBehavior()
|
||||
{
|
||||
if (IsProcessActive())
|
||||
{
|
||||
TerminateProcess(m_hProcess, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
|
||||
Reference in New Issue
Block a user