diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs
new file mode 100644
index 000000000..b25df5d14
--- /dev/null
+++ b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs
@@ -0,0 +1,144 @@
+using System.Runtime.InteropServices;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Hooking.WndProcHook;
+
+///
+/// Event arguments for ,
+/// and the manager for individual WndProc hook.
+///
+internal sealed unsafe class WndProcEventArgs
+{
+ private readonly WndProcHookManager owner;
+ private readonly delegate* unmanaged oldWndProcW;
+ private readonly WndProcDelegate myWndProc;
+
+ private GCHandle gcHandle;
+ private bool released;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The owner.
+ /// The handle of the target window of the message.
+ /// The viewport ID.
+ internal WndProcEventArgs(WndProcHookManager owner, HWND hwnd, int viewportId)
+ {
+ this.Hwnd = hwnd;
+ this.owner = owner;
+ this.ViewportId = viewportId;
+ this.myWndProc = this.WndProcDetour;
+ this.oldWndProcW = (delegate* unmanaged)SetWindowLongPtrW(
+ hwnd,
+ GWLP.GWLP_WNDPROC,
+ Marshal.GetFunctionPointerForDelegate(this.myWndProc));
+ this.gcHandle = GCHandle.Alloc(this);
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam);
+
+ ///
+ /// Gets the handle of the target window of the message.
+ ///
+ public HWND Hwnd { get; }
+
+ ///
+ /// Gets the ImGui viewport ID.
+ ///
+ public int ViewportId { get; }
+
+ ///
+ /// Gets or sets the message.
+ ///
+ public uint Message { get; set; }
+
+ ///
+ /// Gets or sets the WPARAM.
+ ///
+ public WPARAM WParam { get; set; }
+
+ ///
+ /// Gets or sets the LPARAM.
+ ///
+ public LPARAM LParam { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
+ /// Does nothing if changed from .
+ ///
+ public bool SuppressCall { get; set; }
+
+ ///
+ /// Gets or sets the return value.
+ /// Has the return value from next window procedure, if accessed from .
+ ///
+ public LRESULT ReturnValue { get; set; }
+
+ ///
+ /// Sets to true and sets .
+ ///
+ /// The new return value.
+ public void SuppressWithValue(LRESULT returnValue)
+ {
+ this.ReturnValue = returnValue;
+ this.SuppressCall = true;
+ }
+
+ ///
+ /// Sets to true and sets from the result of
+ /// .
+ ///
+ public void SuppressWithDefault()
+ {
+ this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam);
+ this.SuppressCall = true;
+ }
+
+ ///
+ internal void InternalRelease()
+ {
+ if (this.released)
+ return;
+
+ this.released = true;
+ SendMessageW(this.Hwnd, WM.WM_NULL, 0, 0);
+ this.FinalRelease();
+ }
+
+ private void FinalRelease()
+ {
+ if (!this.gcHandle.IsAllocated)
+ return;
+
+ this.gcHandle.Free();
+ SetWindowLongPtrW(this.Hwnd, GWLP.GWLP_WNDPROC, (nint)this.oldWndProcW);
+ this.owner.OnHookedWindowRemoved(this);
+ }
+
+ private LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam)
+ {
+ if (hwnd != this.Hwnd)
+ return CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam);
+
+ this.SuppressCall = false;
+ this.ReturnValue = 0;
+ this.Message = uMsg;
+ this.WParam = wParam;
+ this.LParam = lParam;
+ this.owner.InvokePreWndProc(this);
+
+ if (!this.SuppressCall)
+ this.ReturnValue = CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam);
+
+ this.owner.InvokePostWndProc(this);
+
+ if (uMsg == WM.WM_NCDESTROY || this.released)
+ this.FinalRelease();
+
+ return this.ReturnValue;
+ }
+}
diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs
new file mode 100644
index 000000000..f753f16cc
--- /dev/null
+++ b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs
@@ -0,0 +1,7 @@
+namespace Dalamud.Hooking.WndProcHook;
+
+///
+/// Delegate for overriding WndProc.
+///
+/// The arguments.
+internal delegate void WndProcEventDelegate(WndProcEventArgs args);
diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs
new file mode 100644
index 000000000..00934f27f
--- /dev/null
+++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+using Dalamud.Interface.Utility;
+using Dalamud.Logging.Internal;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Hooking.WndProcHook;
+
+///
+/// Manages WndProc hooks for game main window and extra ImGui viewport windows.
+///
+[ServiceManager.BlockingEarlyLoadedService]
+internal sealed class WndProcHookManager : IServiceType, IDisposable
+{
+ private static readonly ModuleLog Log = new(nameof(WndProcHookManager));
+
+ private readonly Hook dispatchMessageWHook;
+ private readonly Dictionary wndProcOverrides = new();
+
+ [ServiceManager.ServiceConstructor]
+ private unsafe WndProcHookManager()
+ {
+ this.dispatchMessageWHook = Hook.FromImport(
+ null,
+ "user32.dll",
+ "DispatchMessageW",
+ 0,
+ this.DispatchMessageWDetour);
+ this.dispatchMessageWHook.Enable();
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ private unsafe delegate nint DispatchMessageWDelegate(MSG* msg);
+
+ ///
+ /// Called before WndProc.
+ ///
+ public event WndProcEventDelegate? PreWndProc;
+
+ ///
+ /// Called after WndProc.
+ ///
+ public event WndProcEventDelegate? PostWndProc;
+
+ ///
+ public void Dispose()
+ {
+ this.dispatchMessageWHook.Dispose();
+ foreach (var v in this.wndProcOverrides.Values)
+ v.InternalRelease();
+ this.wndProcOverrides.Clear();
+ }
+
+ ///
+ /// Invokes .
+ ///
+ /// The arguments.
+ internal void InvokePreWndProc(WndProcEventArgs args)
+ {
+ try
+ {
+ this.PreWndProc?.Invoke(args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"{nameof(this.PreWndProc)} error");
+ }
+ }
+
+ ///
+ /// Invokes .
+ ///
+ /// The arguments.
+ internal void InvokePostWndProc(WndProcEventArgs args)
+ {
+ try
+ {
+ this.PostWndProc?.Invoke(args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"{nameof(this.PostWndProc)} error");
+ }
+ }
+
+ ///
+ /// Removes from the list of known WndProc overrides.
+ ///
+ /// Object to remove.
+ internal void OnHookedWindowRemoved(WndProcEventArgs args)
+ {
+ if (!this.dispatchMessageWHook.IsDisposed)
+ this.wndProcOverrides.Remove(args.Hwnd);
+ }
+
+ ///
+ /// Detour for . Used to discover new windows to hook.
+ ///
+ /// The message.
+ /// The original return value.
+ private unsafe nint DispatchMessageWDetour(MSG* msg)
+ {
+ if (!this.wndProcOverrides.ContainsKey(msg->hwnd)
+ && ImGuiHelpers.FindViewportId(msg->hwnd) is var vpid and >= 0)
+ {
+ this.wndProcOverrides[msg->hwnd] = new(this, msg->hwnd, vpid);
+ }
+
+ return this.dispatchMessageWHook.Original(msg);
+ }
+}
diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs
index b3252546a..9bd9a2498 100644
--- a/Dalamud/Interface/Internal/DalamudIme.cs
+++ b/Dalamud/Interface/Internal/DalamudIme.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Text.Unicode;
using Dalamud.Game.Text;
+using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Utility;
using Dalamud.Logging.Internal;
@@ -77,6 +78,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
{
get
{
+ if (!ImGuiHelpers.IsImGuiInitialized)
+ return true;
if (!ImGui.GetIO().ConfigInputTextCursorBlink)
return true;
ref var textState = ref TextState;
@@ -185,7 +188,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
/// Processes window messages.
///
/// The arguments.
- public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args)
+ public void ProcessImeMessage(WndProcEventArgs args)
{
if (!ImGuiHelpers.IsImGuiInitialized)
return;
@@ -208,11 +211,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE
or IMN.IMN_CHANGECANDIDATE:
this.UpdateImeWindowStatus(hImc);
- args.SuppressAndReturn(0);
+ args.SuppressWithValue(0);
break;
case WM.WM_IME_STARTCOMPOSITION:
- args.SuppressAndReturn(0);
+ args.SuppressWithValue(0);
break;
case WM.WM_IME_COMPOSITION:
@@ -222,22 +225,22 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
this.ReplaceCompositionString(hImc, (uint)args.LParam);
// Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressAndReturn(0);
+ args.SuppressWithValue(0);
break;
case WM.WM_IME_ENDCOMPOSITION:
// Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressAndReturn(0);
+ args.SuppressWithValue(0);
break;
case WM.WM_IME_CONTROL:
// Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressAndReturn(0);
+ args.SuppressWithValue(0);
break;
case WM.WM_IME_REQUEST:
// Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressAndReturn(0);
+ args.SuppressWithValue(0);
break;
case WM.WM_IME_SETCONTEXT:
@@ -298,7 +301,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
return new(data, 0, numBytes / 2);
}
- private void ReleaseUnmanagedResources() => ImGui.GetIO().SetPlatformImeDataFn = nint.Zero;
+ private void ReleaseUnmanagedResources()
+ {
+ if (ImGuiHelpers.IsImGuiInitialized)
+ ImGui.GetIO().SetPlatformImeDataFn = nint.Zero;
+ }
private void UpdateInputLanguage(HIMC hImc)
{
@@ -492,8 +499,16 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
}
[ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")]
- private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) =>
+ private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
+ {
+ if (!ImGuiHelpers.IsImGuiInitialized)
+ {
+ throw new InvalidOperationException(
+ $"Expected {nameof(InterfaceManager.InterfaceManagerWithScene)} to have initialized ImGui.");
+ }
+
ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate);
+ }
///
/// Ported from imstb_textedit.h.
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 49dfdb248..48157fa86 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -15,6 +15,7 @@ using Dalamud.Game.ClientState.GamePad;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Internal.DXGI;
using Dalamud.Hooking;
+using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Notifications;
@@ -659,16 +660,13 @@ internal class InterfaceManager : IDisposable, IServiceType
this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
}
- private unsafe void WndProcHookManagerOnPreWndProc(ref WndProcHookManager.WndProcOverrideEventArgs args)
+ private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
{
var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam);
if (r is not null)
- {
- args.ReturnValue = r.Value;
- args.SuppressCall = true;
- }
+ args.SuppressWithValue(r.Value);
- this.dalamudIme.ProcessImeMessage(ref args);
+ this.dalamudIme.ProcessImeMessage(args);
}
/*
diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs
deleted file mode 100644
index 1110ff387..000000000
--- a/Dalamud/Interface/Internal/WndProcHookManager.cs
+++ /dev/null
@@ -1,275 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-using Dalamud.Hooking;
-using Dalamud.Interface.Utility;
-using Dalamud.Logging.Internal;
-
-using TerraFX.Interop.Windows;
-
-using static TerraFX.Interop.Windows.Windows;
-
-namespace Dalamud.Interface.Internal;
-
-///
-/// A manifestation of "I can't believe this is required".
-///
-[ServiceManager.BlockingEarlyLoadedService]
-internal sealed class WndProcHookManager : IServiceType, IDisposable
-{
- private static readonly ModuleLog Log = new("WPHM");
-
- private readonly Hook dispatchMessageWHook;
- private readonly Dictionary wndProcNextDict = new();
- private readonly WndProcDelegate wndProcDelegate;
- private readonly uint unhookSelfMessage;
- private bool disposed;
-
- [ServiceManager.ServiceConstructor]
- private unsafe WndProcHookManager()
- {
- this.wndProcDelegate = this.WndProcDetour;
- this.dispatchMessageWHook = Hook.FromImport(
- null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour);
- this.dispatchMessageWHook.Enable();
- fixed (void* pMessageName = $"{nameof(WndProcHookManager)}.{nameof(this.unhookSelfMessage)}")
- this.unhookSelfMessage = RegisterWindowMessageW((ushort*)pMessageName);
- }
-
- ///
- /// Finalizes an instance of the class.
- ///
- ~WndProcHookManager() => this.ReleaseUnmanagedResources();
-
- ///
- /// Delegate for overriding WndProc.
- ///
- /// The arguments.
- public delegate void WndProcOverrideDelegate(ref WndProcOverrideEventArgs args);
-
- [UnmanagedFunctionPointer(CallingConvention.StdCall)]
- private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam);
-
- [UnmanagedFunctionPointer(CallingConvention.StdCall)]
- private delegate nint DispatchMessageWDelegate(ref MSG msg);
-
- ///
- /// Called before WndProc.
- ///
- public event WndProcOverrideDelegate? PreWndProc;
-
- ///
- /// Called after WndProc.
- ///
- public event WndProcOverrideDelegate? PostWndProc;
-
- ///
- public void Dispose()
- {
- this.disposed = true;
- this.dispatchMessageWHook.Dispose();
- this.ReleaseUnmanagedResources();
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Detour for . Used to discover new windows to hook.
- ///
- /// The message.
- /// The original return value.
- private unsafe nint DispatchMessageWDetour(ref MSG msg)
- {
- lock (this.wndProcNextDict)
- {
- if (!this.disposed && ImGuiHelpers.FindViewportId(msg.hwnd) >= 0 &&
- !this.wndProcNextDict.ContainsKey(msg.hwnd))
- {
- this.wndProcNextDict[msg.hwnd] = SetWindowLongPtrW(
- msg.hwnd,
- GWLP.GWLP_WNDPROC,
- Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate));
- }
- }
-
- return this.dispatchMessageWHook.IsDisposed
- ? DispatchMessageW((MSG*)Unsafe.AsPointer(ref msg))
- : this.dispatchMessageWHook.Original(ref msg);
- }
-
- private unsafe LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam)
- {
- nint nextProc;
- lock (this.wndProcNextDict)
- {
- if (!this.wndProcNextDict.TryGetValue(hwnd, out nextProc))
- {
- // Something went wrong; prevent crash. Things will, regardless of the effort, break.
- return DefWindowProcW(hwnd, uMsg, wParam, lParam);
- }
- }
-
- if (uMsg == this.unhookSelfMessage)
- {
- // Even though this message is dedicated for our processing,
- // satisfy the expectations by calling the next window procedure.
- var rv = CallWindowProcW(
- (delegate* unmanaged)nextProc,
- hwnd,
- uMsg,
- wParam,
- lParam);
-
- // Remove self from the chain.
- SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc);
- lock (this.wndProcNextDict)
- this.wndProcNextDict.Remove(hwnd);
-
- return rv;
- }
-
- var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam);
- try
- {
- this.PreWndProc?.Invoke(ref arg);
- }
- catch (Exception e)
- {
- Log.Error(e, $"{nameof(this.PostWndProc)} error");
- }
-
- if (!arg.SuppressCall)
- {
- try
- {
- arg.ReturnValue = CallWindowProcW(
- (delegate* unmanaged)nextProc,
- hwnd,
- uMsg,
- wParam,
- lParam);
- }
- catch (Exception e)
- {
- Log.Error(e, $"{nameof(CallWindowProcW)} error; probably some other software's fault");
- }
-
- try
- {
- this.PostWndProc?.Invoke(ref arg);
- }
- catch (Exception e)
- {
- Log.Error(e, $"{nameof(this.PostWndProc)} error");
- }
- }
-
- if (uMsg == WM.WM_NCDESTROY)
- {
- // The window will cease to exist, once we return.
- SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc);
- lock (this.wndProcNextDict)
- this.wndProcNextDict.Remove(hwnd);
- }
-
- return arg.ReturnValue;
- }
-
- private void ReleaseUnmanagedResources()
- {
- this.disposed = true;
-
- // As wndProcNextDict will be touched on each SendMessageW call, make a copy of window list first.
- HWND[] windows;
- lock (this.wndProcNextDict)
- windows = this.wndProcNextDict.Keys.ToArray();
-
- // Unregister our hook from all the windows we hooked.
- foreach (var v in windows)
- SendMessageW(v, this.unhookSelfMessage, default, default);
- }
-
- ///
- /// Parameters for .
- ///
- public ref struct WndProcOverrideEventArgs
- {
- ///
- /// The handle of the target window of the message.
- ///
- public readonly HWND Hwnd;
-
- ///
- /// The message.
- ///
- public ref uint Message;
-
- ///
- /// The WPARAM.
- ///
- public ref WPARAM WParam;
-
- ///
- /// The LPARAM.
- ///
- public ref LPARAM LParam;
-
- ///
- /// Initializes a new instance of the struct.
- ///
- /// The handle of the target window of the message.
- /// The message.
- /// The WPARAM.
- /// The LPARAM.
- public WndProcOverrideEventArgs(HWND hwnd, ref uint msg, ref WPARAM wParam, ref LPARAM lParam)
- {
- this.Hwnd = hwnd;
- this.LParam = ref lParam;
- this.WParam = ref wParam;
- this.Message = ref msg;
- this.ViewportId = ImGuiHelpers.FindViewportId(hwnd);
- }
-
- ///
- /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
- /// Does nothing if changed from .
- ///
- public bool SuppressCall { get; set; }
-
- ///
- /// Gets or sets the return value.
- /// Has the return value from next window procedure, if accessed from .
- ///
- public LRESULT ReturnValue { get; set; }
-
- ///
- /// Gets the ImGui viewport ID.
- ///
- public int ViewportId { get; init; }
-
- ///
- /// Gets a value indicating whether this message is for the game window (the first viewport).
- ///
- public bool IsGameWindow => this.ViewportId == 0;
-
- ///
- /// Sets to true and sets .
- ///
- /// The new return value.
- public void SuppressAndReturn(LRESULT returnValue)
- {
- this.ReturnValue = returnValue;
- this.SuppressCall = true;
- }
-
- ///
- /// Sets to true and calls .
- ///
- public void SuppressWithDefault()
- {
- this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam);
- this.SuppressCall = true;
- }
- }
-}