adjust lifecycle

This commit is contained in:
Lightczx
2024-05-11 16:23:52 +08:00
parent 0556373bcf
commit dafd3128c2
24 changed files with 364 additions and 313 deletions

View File

@@ -8,7 +8,9 @@ using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Shell;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Core.Windowing.NotifyIcon;
using System.Diagnostics;
namespace Snap.Hutao;
@@ -42,6 +44,8 @@ public sealed partial class App : Application
private readonly IActivation activation;
private readonly ILogger<App> logger;
private NotifyIconController? notifyIconController;
/// <summary>
/// Initializes the singleton application object.
/// </summary>
@@ -75,11 +79,9 @@ public sealed partial class App : Application
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
LogDiagnosticInformation();
// manually invoke
// Manually invoke
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.Initialize();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
activation.PostInitialization();
}
catch
{

View File

@@ -5,6 +5,8 @@ using CommunityToolkit.WinUI.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Core.Windowing.NotifyIcon;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Hutao;
@@ -22,7 +24,7 @@ namespace Snap.Hutao.Core.LifeCycle;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IActivation))]
[SuppressMessage("", "CA1001")]
internal sealed partial class Activation : IActivation
internal sealed partial class Activation : IActivation, IDisposable
{
public const string Action = nameof(Action);
public const string Uid = nameof(Uid);
@@ -42,6 +44,8 @@ internal sealed partial class Activation : IActivation
/// <inheritdoc/>
public void Activate(HutaoActivationArguments args)
{
// Before activate, we try to redirect to the opened process in App,
// And we check if it's a toast activation.
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
return;
@@ -51,10 +55,21 @@ internal sealed partial class Activation : IActivation
}
/// <inheritdoc/>
public void Initialize()
public void PostInitialization()
{
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
serviceProvider.GetRequiredService<HotKeyOptions>().RegisterAll();
if (LocalSetting.Get(SettingKeys.IsNotifyIconEnabled, true))
{
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
}
public void Dispose()
{
activateSemaphore.Dispose();
}
private void NotificationActivate(ToastNotificationActivatedEventArgsCompat args)
@@ -94,12 +109,6 @@ internal sealed partial class Activation : IActivation
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
switch (args.LaunchActivatedArguments)
{
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
@@ -112,10 +121,9 @@ internal sealed partial class Activation : IActivation
private async ValueTask HandleNormalLaunchActionAsync()
{
// Increase launch times
LocalSetting.Update(SettingKeys.LaunchTimes, 0, x => x + 1);
LocalSetting.Update(SettingKeys.LaunchTimes, 0, x => unchecked(x + 1));
// If it's the first time launch, we show the guide window anyway.
// Otherwise, we check if there's any unfulfilled resource category present.
// If the guide is completed, we check if there's any unfulfilled resource category present.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
{
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
@@ -124,6 +132,7 @@ internal sealed partial class Activation : IActivation
}
}
// If it's the first time launch, show the guide window anyway.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
await taskContext.SwitchToMainThreadAsync();
@@ -158,10 +167,7 @@ internal sealed partial class Activation : IActivation
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget();
}
serviceProvider
.GetRequiredService<IDiscordService>()
.SetNormalActivityAsync()
.SafeForget();
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget();
}
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)

View File

@@ -17,7 +17,7 @@ internal static class CurrentWindowReferenceExtension
public static HWND GetWindowHandle(this ICurrentWindowReference reference)
{
return reference.Window is IWindowOptionsSource optionsSource
return reference.Window is IXamlWindowOptionsSource optionsSource
? optionsSource.WindowOptions.Hwnd
: WindowNative.GetWindowHandle(reference.Window);
}

View File

@@ -10,5 +10,5 @@ internal interface IActivation
{
void Activate(HutaoActivationArguments args);
void Initialize();
void PostInitialization();
}

View File

@@ -26,6 +26,7 @@ internal static class SettingKeys
public const string StaticResourceImageArchive = "StaticResourceImageArchive";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
public const string IsNotifyIconEnabled = "IsNotifyIconEnabled";
#endregion
#region Passport

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Shell;
/// <summary>
/// 跳转列表交互
/// </summary>
internal interface IJumpListInterop
{
/// <summary>
/// 异步配置跳转列表
/// </summary>
/// <returns>任务</returns>
ValueTask ConfigureAsync();
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.LifeCycle;
using Windows.UI.StartScreen;
namespace Snap.Hutao.Core.Shell;
/// <summary>
/// 跳转列表交互
/// </summary>
[HighQuality]
[Injection(InjectAs.Transient, typeof(IJumpListInterop))]
internal sealed class JumpListInterop : IJumpListInterop
{
/// <summary>
/// 异步配置跳转列表
/// </summary>
/// <returns>任务</returns>
public async ValueTask ConfigureAsync()
{
if (JumpList.IsSupported())
{
JumpList list = await JumpList.LoadCurrentAsync();
list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, SH.CoreJumpListHelperLaunchGameItemDisplayName);
launchGameItem.Logo = "ms-appx:///Resource/Navigation/LaunchGame.png".ToUri();
list.Items.Add(launchGameItem);
await list.SaveAsync();
}
}
}

View File

@@ -17,10 +17,10 @@ namespace Snap.Hutao.Core.Windowing.HotKey;
[SuppressMessage("", "SA1124")]
internal sealed class HotKeyCombination : ObservableObject
{
private readonly ICurrentWindowReference currentWindowReference;
private readonly IInfoBarService infoBarService;
private readonly RuntimeOptions runtimeOptions;
private readonly HWND hwnd;
private readonly string settingKey;
private readonly int hotKeyId;
private readonly HotKeyParameter defaultHotKeyParameter;
@@ -36,12 +36,12 @@ internal sealed class HotKeyCombination : ObservableObject
private VirtualKey key;
private bool isEnabled;
public HotKeyCombination(IServiceProvider serviceProvider, string settingKey, int hotKeyId, HOT_KEY_MODIFIERS defaultModifiers, VirtualKey defaultKey)
public HotKeyCombination(IServiceProvider serviceProvider, HWND hwnd, string settingKey, int hotKeyId, HOT_KEY_MODIFIERS defaultModifiers, VirtualKey defaultKey)
{
currentWindowReference = serviceProvider.GetRequiredService<ICurrentWindowReference>();
infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
this.hwnd = hwnd;
this.settingKey = settingKey;
this.hotKeyId = hotKeyId;
defaultHotKeyParameter = new(defaultModifiers, defaultKey);
@@ -53,7 +53,7 @@ internal sealed class HotKeyCombination : ObservableObject
HotKeyParameter actual = LocalSettingGetHotKeyParameter();
modifiers = actual.Modifiers;
InitializeModifiersComposeFields();
InitializeModifiersCompositionFields();
key = actual.Key;
keyNameValue = VirtualKeys.GetList().Single(v => v.Value == key);
@@ -164,8 +164,8 @@ internal sealed class HotKeyCombination : ObservableObject
_ = (value, registered) switch
{
(true, false) => RegisterForCurrentWindow(),
(false, true) => UnregisterForCurrentWindow(),
(true, false) => Register(),
(false, true) => Unregister(),
_ => false,
};
}
@@ -174,7 +174,7 @@ internal sealed class HotKeyCombination : ObservableObject
public string DisplayName { get => ToString(); }
public bool RegisterForCurrentWindow()
public bool Register()
{
if (!runtimeOptions.IsElevated || !IsEnabled)
{
@@ -186,7 +186,6 @@ internal sealed class HotKeyCombination : ObservableObject
return true;
}
HWND hwnd = currentWindowReference.GetWindowHandle();
BOOL result = RegisterHotKey(hwnd, hotKeyId, Modifiers, (uint)Key);
registered = result;
@@ -198,7 +197,7 @@ internal sealed class HotKeyCombination : ObservableObject
return result;
}
public bool UnregisterForCurrentWindow()
public bool Unregister()
{
if (!runtimeOptions.IsElevated)
{
@@ -210,7 +209,6 @@ internal sealed class HotKeyCombination : ObservableObject
return true;
}
HWND hwnd = currentWindowReference.GetWindowHandle();
BOOL result = UnregisterHotKey(hwnd, hotKeyId);
registered = !result;
return result;
@@ -272,7 +270,7 @@ internal sealed class HotKeyCombination : ObservableObject
Modifiers = modifiers;
}
private void InitializeModifiersComposeFields()
private void InitializeModifiersCompositionFields()
{
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_WIN))
{
@@ -309,7 +307,7 @@ internal sealed class HotKeyCombination : ObservableObject
HotKeyParameter current = new(Modifiers, Key);
LocalSetting.Set(settingKey, *(int*)&current);
UnregisterForCurrentWindow();
RegisterForCurrentWindow();
Unregister();
Register();
}
}

View File

@@ -1,97 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
using System.Runtime.InteropServices;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing.HotKey;
[SuppressMessage("", "CA1001")]
[Injection(InjectAs.Singleton, typeof(IHotKeyController))]
[ConstructorGenerated]
internal sealed partial class HotKeyController : IHotKeyController
{
private static readonly WaitCallback RunMouseClickRepeatForever = MouseClickRepeatForever;
private readonly object syncRoot = new();
private readonly HotKeyOptions hotKeyOptions;
private volatile CancellationTokenSource? cancellationTokenSource;
public void RegisterAll()
{
hotKeyOptions.MouseClickRepeatForeverKeyCombination.RegisterForCurrentWindow();
}
public void UnregisterAll()
{
hotKeyOptions.MouseClickRepeatForeverKeyCombination.UnregisterForCurrentWindow();
}
public void OnHotKeyPressed(in HotKeyParameter parameter)
{
if (parameter.Equals(hotKeyOptions.MouseClickRepeatForeverKeyCombination))
{
ToggleMouseClickRepeatForever();
}
}
private static unsafe INPUT CreateInputForMouseEvent(MOUSE_EVENT_FLAGS flags)
{
INPUT input = default;
input.type = INPUT_TYPE.INPUT_MOUSE;
input.Anonymous.mi.dwFlags = flags;
return input;
}
[SuppressMessage("", "SH007")]
private static unsafe void MouseClickRepeatForever(object? state)
{
CancellationToken token = (CancellationToken)state!;
// We want to use this thread for a long time
while (!token.IsCancellationRequested)
{
INPUT[] inputs =
[
CreateInputForMouseEvent(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN),
CreateInputForMouseEvent(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP),
];
if (SendInput(inputs.AsSpan(), sizeof(INPUT)) is 0)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
}
if (token.IsCancellationRequested)
{
return;
}
Thread.Sleep(System.Random.Shared.Next(100, 150));
}
}
private void ToggleMouseClickRepeatForever()
{
lock (syncRoot)
{
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
{
// Turn off
cancellationTokenSource?.Cancel();
cancellationTokenSource = default;
hotKeyOptions.IsMouseClickRepeatForeverOn = false;
}
else
{
// Turn on
cancellationTokenSource = new();
ThreadPool.QueueUserWorkItem(RunMouseClickRepeatForever, cancellationTokenSource.Token);
hotKeyOptions.IsMouseClickRepeatForeverOn = true;
}
}
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Snap.Hutao.Win32.ConstValues;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing.HotKey;
internal sealed class HotKeyMessageWindow : IDisposable
{
private const string WindowClassName = "SnapHutaoHotKeyMessageWindowClass";
private static readonly ConcurrentDictionary<HWND, HotKeyMessageWindow> WindowTable = [];
private bool isDisposed;
public unsafe HotKeyMessageWindow()
{
ushort atom;
fixed (char* className = WindowClassName)
{
WNDCLASSW wc = new()
{
lpfnWndProc = WNDPROC.Create(&OnWindowProcedure),
lpszClassName = className,
};
atom = RegisterClassW(&wc);
}
ArgumentOutOfRangeException.ThrowIfEqual<ushort>(atom, 0);
HWND = CreateWindowExW(0, WindowClassName, WindowClassName, 0, 0, 0, 0, 0, default, default, default, default);
if (HWND == default)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
}
WindowTable.TryAdd(HWND, this);
}
~HotKeyMessageWindow()
{
Dispose();
}
public Action<HotKeyParameter>? HotKeyPressed { get; set; }
public HWND HWND { get; }
public void Dispose()
{
if (isDisposed)
{
return;
}
isDisposed = true;
DestroyWindow(HWND);
WindowTable.TryRemove(HWND, out _);
GC.SuppressFinalize(this);
}
[SuppressMessage("", "SH002")]
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static unsafe LRESULT OnWindowProcedure(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
if (!WindowTable.TryGetValue(hwnd, out HotKeyMessageWindow? window))
{
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
switch (uMsg)
{
case WM_HOTKEY:
window.HotKeyPressed?.Invoke(*(HotKeyParameter*)&lParam);
break;
default:
break;
}
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
}

View File

@@ -4,19 +4,35 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
using System.Runtime.InteropServices;
using Windows.System;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing.HotKey;
[Injection(InjectAs.Singleton)]
internal sealed partial class HotKeyOptions : ObservableObject
internal sealed partial class HotKeyOptions : ObservableObject, IDisposable
{
private static readonly WaitCallback RunMouseClickRepeatForever = MouseClickRepeatForever;
private readonly object syncRoot = new();
private readonly HotKeyMessageWindow hotKeyMessageWindow;
private volatile CancellationTokenSource? cancellationTokenSource;
private bool isDisposed;
private bool isMouseClickRepeatForeverOn;
private HotKeyCombination mouseClickRepeatForeverKeyCombination;
public HotKeyOptions(IServiceProvider serviceProvider)
{
mouseClickRepeatForeverKeyCombination = new(serviceProvider, SettingKeys.HotKeyMouseClickRepeatForever, 100000, default, VirtualKey.F8);
hotKeyMessageWindow = new()
{
HotKeyPressed = OnHotKeyPressed,
};
mouseClickRepeatForeverKeyCombination = new(serviceProvider, hotKeyMessageWindow.HWND, SettingKeys.HotKeyMouseClickRepeatForever, 100000, default, VirtualKey.F8);
}
public List<NameValue<VirtualKey>> VirtualKeys { get; } = HotKey.VirtualKeys.GetList();
@@ -32,4 +48,96 @@ internal sealed partial class HotKeyOptions : ObservableObject
get => mouseClickRepeatForeverKeyCombination;
set => SetProperty(ref mouseClickRepeatForeverKeyCombination, value);
}
public void Dispose()
{
if (isDisposed)
{
return;
}
isDisposed = true;
UnregisterAll();
hotKeyMessageWindow.Dispose();
cancellationTokenSource?.Dispose();
GC.SuppressFinalize(this);
}
public void RegisterAll()
{
MouseClickRepeatForeverKeyCombination.Register();
}
private static unsafe INPUT CreateInputForMouseEvent(MOUSE_EVENT_FLAGS flags)
{
INPUT input = default;
input.type = INPUT_TYPE.INPUT_MOUSE;
input.Anonymous.mi.dwFlags = flags;
return input;
}
[SuppressMessage("", "SH007")]
private static unsafe void MouseClickRepeatForever(object? state)
{
CancellationToken token = (CancellationToken)state!;
// We want to use this thread for a long time
while (!token.IsCancellationRequested)
{
INPUT[] inputs =
[
CreateInputForMouseEvent(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN),
CreateInputForMouseEvent(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP),
];
if (SendInput(inputs.AsSpan(), sizeof(INPUT)) is 0)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
}
if (token.IsCancellationRequested)
{
return;
}
Thread.Sleep(System.Random.Shared.Next(100, 150));
}
}
private void UnregisterAll()
{
MouseClickRepeatForeverKeyCombination.Unregister();
}
[SuppressMessage("", "SH002")]
private void OnHotKeyPressed(HotKeyParameter parameter)
{
if (parameter.Equals(MouseClickRepeatForeverKeyCombination))
{
ToggleMouseClickRepeatForever();
}
}
private void ToggleMouseClickRepeatForever()
{
lock (syncRoot)
{
if (IsMouseClickRepeatForeverOn)
{
// Turn off
cancellationTokenSource?.Cancel();
cancellationTokenSource = default;
IsMouseClickRepeatForeverOn = false;
}
else
{
// Turn on
cancellationTokenSource = new();
ThreadPool.QueueUserWorkItem(RunMouseClickRepeatForever, cancellationTokenSource.Token);
IsMouseClickRepeatForeverOn = true;
}
}
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Windowing.HotKey;
internal interface IHotKeyController
{
void OnHotKeyPressed(in HotKeyParameter parameter);
void RegisterAll();
void UnregisterAll();
}

View File

@@ -6,10 +6,10 @@ namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 为扩展窗体提供必要的选项
/// </summary>
internal interface IWindowOptionsSource
internal interface IXamlWindowOptionsSource
{
/// <summary>
/// 窗体选项
/// </summary>
WindowOptions WindowOptions { get; }
XamlWindowOptions WindowOptions { get; }
}

View File

@@ -10,6 +10,7 @@ using static Snap.Hutao.Win32.ConstValues;
namespace Snap.Hutao.Core.Windowing.NotifyIcon;
[Injection(InjectAs.Singleton)]
internal sealed class NotifyIconController : IDisposable
{
private readonly NotifyIconMessageWindow messageWindow;
@@ -17,13 +18,23 @@ internal sealed class NotifyIconController : IDisposable
public NotifyIconController()
{
messageWindow = new();
NotifyIconMethods.Delete(Id);
StorageFile iconFile = StorageFile.GetFileFromApplicationUriAsync("ms-appx:///Assets/Logo.ico".ToUri()).AsTask().GetAwaiter().GetResult();
icon = new(iconFile.Path);
messageWindow = new()
{
TaskbarCreated = window =>
{
NotifyIconMethods.Delete(Id);
if (!NotifyIconMethods.Add(Id, window.HWND, "Snap Hutao", NotifyIconMessageWindow.WM_NOTIFYICON_CALLBACK, (HICON)icon.Handle))
{
HutaoException.InvalidOperation("Failed to recreate NotifyIcon");
}
},
};
NotifyIconMethods.Delete(Id);
if (!NotifyIconMethods.Add(Id, messageWindow.HWND, "Snap Hutao", NotifyIconMessageWindow.WM_NOTIFYICON_CALLBACK, (HICON)icon.Handle))
{
HutaoException.InvalidOperation("Failed to create NotifyIcon");

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
@@ -19,12 +21,10 @@ internal sealed class NotifyIconMessageWindow : IDisposable
public const uint WM_NOTIFYICON_CALLBACK = 0x444U;
private const string WindowClassName = "SnapHutaoNotifyIconMessageWindowClass";
public readonly HWND HWND;
private static readonly ConcurrentDictionary<HWND, NotifyIconMessageWindow> WindowTable = [];
[SuppressMessage("", "SA1306")]
private uint WM_TASKBARCREATED;
private readonly uint WM_TASKBARCREATED;
private bool isDisposed;
@@ -62,6 +62,10 @@ internal sealed class NotifyIconMessageWindow : IDisposable
Dispose();
}
public Action<NotifyIconMessageWindow>? TaskbarCreated { get; set; }
public HWND HWND { get; }
public void Dispose()
{
if (isDisposed)
@@ -81,59 +85,62 @@ internal sealed class NotifyIconMessageWindow : IDisposable
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static unsafe LRESULT OnWindowProcedure(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
if (WindowTable.TryGetValue(hwnd, out NotifyIconMessageWindow? window))
if (!WindowTable.TryGetValue(hwnd, out NotifyIconMessageWindow? window))
{
if (uMsg == window.WM_TASKBARCREATED)
{
// TODO: Re-add the notify icon.
}
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
// https://learn.microsoft.com/zh-cn/windows/win32/api/shellapi/ns-shellapi-notifyicondataw
if (uMsg is WM_NOTIFYICON_CALLBACK)
{
LPARAM2 lParam2 = *(LPARAM2*)&lParam;
WPARAM2 wParam2 = *(WPARAM2*)&wParam;
if (uMsg == window.WM_TASKBARCREATED)
{
// TODO: Re-add the notify icon.
window.TaskbarCreated?.Invoke(window);
}
switch (lParam2.Low)
{
case WM_MOUSEMOVE:
// X: wParam2.X Y: wParam2.Y Low: WM_MOUSEMOVE
break;
case NIN_SELECT:
// X: wParam2.X Y: wParam2.Y Low: NIN_SELECT
break;
case NIN_POPUPOPEN:
// X: wParam2.X Y: 0? Low: NIN_POPUPOPEN
break;
case NIN_POPUPCLOSE:
// X: wParam2.X Y: 0? Low: NIN_POPUPCLOSE
break;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
break;
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
break;
case WM_CONTEXTMENU:
Debug.WriteLine($"[uMsg: 0x{uMsg:X8}] [X: {wParam2.X} Y: {wParam2.Y}] [Low: WM_CONTEXTMENU High: 0x{lParam2.High:X8}]");
break;
default:
Debug.WriteLine($"[uMsg: 0x{uMsg:X8}] [X: {wParam2.X} Y: {wParam2.Y}] [Low: 0x{lParam2.Low:X8} High: 0x{lParam2.High:X8}]");
break;
}
}
else
// https://learn.microsoft.com/zh-cn/windows/win32/api/shellapi/ns-shellapi-notifyicondataw
if (uMsg is WM_NOTIFYICON_CALLBACK)
{
LPARAM2 lParam2 = *(LPARAM2*)&lParam;
WPARAM2 wParam2 = *(WPARAM2*)&wParam;
switch (lParam2.Low)
{
switch (uMsg)
{
case WM_ACTIVATEAPP:
break;
case WM_DWMNCRENDERINGCHANGED:
break;
default:
Debug.WriteLine($"[uMsg: 0x{uMsg:X8}] [wParam: 0x{wParam.Value:X8}] [lParam: 0x{lParam.Value:X8}]");
break;
}
case WM_MOUSEMOVE:
// X: wParam2.X Y: wParam2.Y Low: WM_MOUSEMOVE
break;
case NIN_SELECT:
// X: wParam2.X Y: wParam2.Y Low: NIN_SELECT
break;
case NIN_POPUPOPEN:
// X: wParam2.X Y: 0? Low: NIN_POPUPOPEN
break;
case NIN_POPUPCLOSE:
// X: wParam2.X Y: 0? Low: NIN_POPUPCLOSE
break;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
break;
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
break;
case WM_CONTEXTMENU:
Debug.WriteLine($"[uMsg: 0x{uMsg:X8}] [X: {wParam2.X} Y: {wParam2.Y}] [Low: WM_CONTEXTMENU High: 0x{lParam2.High:X8}]");
break;
default:
Debug.WriteLine($"[uMsg: 0x{uMsg:X8}] [X: {wParam2.X} Y: {wParam2.Y}] [Low: 0x{lParam2.Low:X8} High: 0x{lParam2.High:X8}]");
break;
}
}
else
{
switch (uMsg)
{
case WM_ACTIVATEAPP:
break;
case WM_DWMNCRENDERINGCHANGED:
break;
default:
Debug.WriteLine($"[uMsg: 0x{uMsg:X8}] [wParam: 0x{wParam.Value:X8}] [lParam: 0x{lParam.Value:X8}]");
break;
}
}
@@ -151,4 +158,18 @@ internal sealed class NotifyIconMessageWindow : IDisposable
public readonly ushort X;
public readonly ushort Y;
}
}
internal sealed class NotifyIconXamlHostWindow : Window
{
public NotifyIconXamlHostWindow()
{
Content = new Border();
OverlappedPresenter presenter = OverlappedPresenter.Create();
presenter.SetBorderAndTitleBar(false, false);
presenter.IsAlwaysOnTop = true;
presenter.IsResizable = false;
AppWindow.SetPresenter(presenter);
}
}

View File

@@ -12,12 +12,12 @@ namespace Snap.Hutao.Core.Windowing;
internal static class WindowExtension
{
private static readonly ConditionalWeakTable<Window, WindowController> WindowControllers = [];
private static readonly ConditionalWeakTable<Window, XamlWindowController> WindowControllers = [];
public static void InitializeController<TWindow>(this TWindow window, IServiceProvider serviceProvider)
where TWindow : Window, IWindowOptionsSource
where TWindow : Window, IXamlWindowOptionsSource
{
WindowController windowController = new(window, window.WindowOptions, serviceProvider);
XamlWindowController windowController = new(window, window.WindowOptions, serviceProvider);
WindowControllers.Add(window, windowController);
}

View File

@@ -22,15 +22,15 @@ using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing;
[SuppressMessage("", "CA1001")]
internal sealed class WindowController
internal sealed class XamlWindowController
{
private readonly Window window;
private readonly WindowOptions options;
private readonly XamlWindowOptions options;
private readonly IServiceProvider serviceProvider;
private readonly WindowSubclass subclass;
private readonly WindowNonRudeHWND windowNonRudeHWND;
private readonly XamlWindowSubclass subclass;
private readonly XamlWindowNonRudeHWND windowNonRudeHWND;
public WindowController(Window window, in WindowOptions options, IServiceProvider serviceProvider)
public XamlWindowController(Window window, in XamlWindowOptions options, IServiceProvider serviceProvider)
{
this.window = window;
this.options = options;
@@ -38,7 +38,7 @@ internal sealed class WindowController
// Window reference must be set before Window Subclass created
serviceProvider.GetRequiredService<ICurrentWindowReference>().Window = window;
subclass = new(window, options, serviceProvider);
subclass = new(window, options);
windowNonRudeHWND = new(options.Hwnd);
InitializeCore();

View File

@@ -6,13 +6,13 @@ using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing;
internal sealed class WindowNonRudeHWND : IDisposable
internal sealed class XamlWindowNonRudeHWND : IDisposable
{
// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist2-markfullscreenwindow#remarks
private const string NonRudeHWND = "NonRudeHWND";
private readonly HWND hwnd;
public WindowNonRudeHWND(HWND hwnd)
public XamlWindowNonRudeHWND(HWND hwnd)
{
this.hwnd = hwnd;
SetPropW(hwnd, NonRudeHWND, BOOL.TRUE);

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// Window 选项
/// </summary>
internal readonly struct WindowOptions
internal readonly struct XamlWindowOptions
{
/// <summary>
/// 窗体句柄
@@ -43,7 +43,7 @@ internal readonly struct WindowOptions
public readonly string? PersistRectKey;
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, string? persistSize = default)
public XamlWindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, string? persistSize = default)
{
Hwnd = WindowNative.GetWindowHandle(window);
InputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing.Backdrop;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Win32;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.UI.Shell;
@@ -15,51 +14,35 @@ using static Snap.Hutao.Win32.ConstValues;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 窗体子类管理器
/// </summary>
[HighQuality]
internal sealed class WindowSubclass : IDisposable
internal sealed class XamlWindowSubclass : IDisposable
{
private const int WindowSubclassId = 101;
private readonly Window window;
private readonly WindowOptions options;
private readonly IServiceProvider serviceProvider;
private readonly IHotKeyController hotKeyController;
private readonly XamlWindowOptions options;
// We have to explicitly hold a reference to SUBCLASSPROC
private SUBCLASSPROC windowProc = default!;
private UnmanagedAccess<WindowSubclass> unmanagedAccess = default!;
private UnmanagedAccess<XamlWindowSubclass> unmanagedAccess = default!;
public WindowSubclass(Window window, in WindowOptions options, IServiceProvider serviceProvider)
public XamlWindowSubclass(Window window, in XamlWindowOptions options)
{
this.window = window;
this.options = options;
this.serviceProvider = serviceProvider;
hotKeyController = serviceProvider.GetRequiredService<IHotKeyController>();
}
/// <summary>
/// 尝试设置窗体子类
/// </summary>
/// <returns>是否设置成功</returns>
public unsafe bool Initialize()
{
windowProc = SUBCLASSPROC.Create(&OnSubclassProcedure);
unmanagedAccess = UnmanagedAccess.Create(this);
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, unmanagedAccess);
hotKeyController.RegisterAll();
return windowHooked;
}
/// <inheritdoc/>
public void Dispose()
{
hotKeyController.UnregisterAll();
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
windowProc = default!;
unmanagedAccess.Dispose();
@@ -69,7 +52,7 @@ internal sealed class WindowSubclass : IDisposable
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static unsafe LRESULT OnSubclassProcedure(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam, nuint uIdSubclass, nuint dwRefData)
{
WindowSubclass? state = UnmanagedAccess.Get<WindowSubclass>(dwRefData);
XamlWindowSubclass? state = UnmanagedAccess.Get<XamlWindowSubclass>(dwRefData);
ArgumentNullException.ThrowIfNull(state);
switch (uMsg)
@@ -90,12 +73,6 @@ internal sealed class WindowSubclass : IDisposable
return default;
}
case WM_HOTKEY:
{
state.hotKeyController.OnHotKeyPressed(*(HotKeyParameter*)&lParam);
break;
}
case WM_ERASEBKGND:
{
if (state.window.SystemBackdrop is IBackdropNeedEraseBackground)

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao;
/// 指引窗口
/// </summary>
[Injection(InjectAs.Singleton)]
internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler
internal sealed partial class GuideWindow : Window, IXamlWindowOptionsSource, IMinMaxInfoHandler
{
private const int MinWidth = 1000;
private const int MinHeight = 650;
@@ -20,7 +20,7 @@ internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMa
private const int MaxWidth = 1200;
private const int MaxHeight = 800;
private readonly WindowOptions windowOptions;
private readonly XamlWindowOptions windowOptions;
public GuideWindow(IServiceProvider serviceProvider)
{
@@ -29,7 +29,7 @@ internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMa
this.InitializeController(serviceProvider);
}
WindowOptions IWindowOptionsSource.WindowOptions { get => windowOptions; }
XamlWindowOptions IXamlWindowOptionsSource.WindowOptions { get => windowOptions; }
public unsafe void HandleMinMaxInfo(ref MINMAXINFO info, double scalingFactor)
{

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao;
/// </summary>
[HighQuality]
[Injection(InjectAs.Singleton)]
internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOptionsSource, IMinMaxInfoHandler
internal sealed partial class LaunchGameWindow : Window, IDisposable, IXamlWindowOptionsSource, IMinMaxInfoHandler
{
private const int MinWidth = 240;
private const int MinHeight = 240;
@@ -23,7 +23,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
private const int MaxWidth = 320;
private const int MaxHeight = 320;
private readonly WindowOptions windowOptions;
private readonly XamlWindowOptions windowOptions;
private readonly IServiceScope scope;
/// <summary>
@@ -41,7 +41,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
}
/// <inheritdoc/>
public WindowOptions WindowOptions { get => windowOptions; }
public XamlWindowOptions WindowOptions { get => windowOptions; }
/// <inheritdoc/>
public void Dispose()

View File

@@ -14,12 +14,12 @@ namespace Snap.Hutao;
[HighQuality]
[Injection(InjectAs.Singleton)]
[SuppressMessage("", "CA1001")]
internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler
internal sealed partial class MainWindow : Window, IXamlWindowOptionsSource, IMinMaxInfoHandler
{
private const int MinWidth = 1000;
private const int MinHeight = 600;
private readonly WindowOptions windowOptions;
private readonly XamlWindowOptions windowOptions;
/// <summary>
/// 构造一个新的主窗体
@@ -33,7 +33,7 @@ internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMax
}
/// <inheritdoc/>
public WindowOptions WindowOptions { get => windowOptions; }
public XamlWindowOptions WindowOptions { get => windowOptions; }
/// <inheritdoc/>
public unsafe void HandleMinMaxInfo(ref MINMAXINFO pInfo, double scalingFactor)

View File

@@ -50,12 +50,9 @@ public static partial class Program
// By adding the using statement, we can dispose the injected services when we closing
using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
{
using (NotifyIconController notifyIconController = new())
{
// In a Desktop app this runs a message pump internally,
// and does not return until the application shuts down.
Application.Start(AppInitializationCallback);
}
// In a Desktop app this runs a message pump internally,
// and does not return until the application shuts down.
Application.Start(AppInitializationCallback);
}
}