From b6d88f798a600c5033887f949b2a3d419e12afa9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 7 Dec 2023 22:41:10 +0900 Subject: [PATCH 01/10] Make CJK imes work better --- Dalamud/Dalamud.cs | 3 +- Dalamud/Game/Gui/Internal/DalamudIME.cs | 301 ---------- Dalamud/Interface/Internal/DalamudIme.cs | 521 ++++++++++++++++++ .../Interface/Internal/DalamudInterface.cs | 10 +- .../Interface/Internal/InterfaceManager.cs | 59 +- .../Internal/Windows/DalamudImeWindow.cs | 223 ++++++++ .../Interface/Internal/Windows/IMEWindow.cs | 120 ---- .../Interface/Internal/WndProcHookManager.cs | 273 +++++++++ Dalamud/Interface/Utility/ImGuiHelpers.cs | 20 + 9 files changed, 1064 insertions(+), 466 deletions(-) delete mode 100644 Dalamud/Game/Gui/Internal/DalamudIME.cs create mode 100644 Dalamud/Interface/Internal/DalamudIme.cs create mode 100644 Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs delete mode 100644 Dalamud/Interface/Internal/Windows/IMEWindow.cs create mode 100644 Dalamud/Interface/Internal/WndProcHookManager.cs diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 9896b87a61..4ab617d0a9 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,7 +9,6 @@ using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; -using Dalamud.Game.Gui.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Storage; @@ -178,7 +177,7 @@ public void DisposePlugins() // this must be done before unloading interface manager, in order to do rebuild // the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game // will not receive any windows messages - Service.GetNullable()?.Dispose(); + Service.GetNullable()?.Dispose(); // this must be done before unloading plugins, or it can cause a race condition // due to rendering happening on another thread, where a plugin might receive diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs deleted file mode 100644 index a9f6991ae0..0000000000 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; - -using Dalamud.Hooking; -using Dalamud.Interface.Internal; -using Dalamud.Logging.Internal; -using ImGuiNET; -using PInvoke; - -using static Dalamud.NativeFunctions; - -namespace Dalamud.Game.Gui.Internal; - -/// -/// This class handles IME for non-English users. -/// -[ServiceManager.EarlyLoadedService] -internal unsafe class DalamudIME : IDisposable, IServiceType -{ - private static readonly ModuleLog Log = new("IME"); - - private AsmHook imguiTextInputCursorHook; - private Vector2* cursorPos; - - [ServiceManager.ServiceConstructor] - private DalamudIME() - { - } - - /// - /// Gets a value indicating whether the module is enabled. - /// - internal bool IsEnabled { get; private set; } - - /// - /// Gets the index of the first imm candidate in relation to the full list. - /// - internal CandidateList ImmCandNative { get; private set; } = default; - - /// - /// Gets the imm candidates. - /// - internal List ImmCand { get; private set; } = new(); - - /// - /// Gets the selected imm component. - /// - internal string ImmComp { get; private set; } = string.Empty; - - /// - public void Dispose() - { - this.imguiTextInputCursorHook?.Dispose(); - Marshal.FreeHGlobal((IntPtr)this.cursorPos); - } - - /// - /// Processes window messages. - /// - /// Handle of the window. - /// Type of window message. - /// wParam or the pointer to it. - /// lParam or the pointer to it. - /// Return value, if not doing further processing. - public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParamPtr, void* lParamPtr) - { - try - { - if (ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput) - { - var io = ImGui.GetIO(); - var wmsg = (WindowsMessage)msg; - long wParam = (long)wParamPtr, lParam = (long)lParamPtr; - try - { - wParam = Marshal.ReadInt32((IntPtr)wParamPtr); - } - catch - { - // ignored - } - - try - { - lParam = Marshal.ReadInt32((IntPtr)lParamPtr); - } - catch - { - // ignored - } - - switch (wmsg) - { - case WindowsMessage.WM_IME_NOTIFY: - switch ((IMECommand)(IntPtr)wParam) - { - case IMECommand.ChangeCandidate: - this.ToggleWindow(true); - this.LoadCand(hWnd); - break; - case IMECommand.OpenCandidate: - this.ToggleWindow(true); - this.ImmCandNative = default; - // this.ImmCand.Clear(); - break; - - case IMECommand.CloseCandidate: - this.ToggleWindow(false); - this.ImmCandNative = default; - // this.ImmCand.Clear(); - break; - - default: - break; - } - - break; - case WindowsMessage.WM_IME_COMPOSITION: - if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause | - IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & (long)(IntPtr)lParam) > 0) - { - var hIMC = ImmGetContext(hWnd); - if (hIMC == IntPtr.Zero) - return IntPtr.Zero; - - var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0); - var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); - ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, unmanagedPointer, (uint)dwSize); - - var bytes = new byte[dwSize]; - Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize); - Marshal.FreeHGlobal(unmanagedPointer); - - var lpstr = Encoding.Unicode.GetString(bytes); - this.ImmComp = lpstr; - if (lpstr == string.Empty) - { - this.ToggleWindow(false); - } - else - { - this.LoadCand(hWnd); - } - } - - if (((long)(IntPtr)lParam & (long)IMEComposition.ResultStr) > 0) - { - var hIMC = ImmGetContext(hWnd); - if (hIMC == IntPtr.Zero) - return IntPtr.Zero; - - var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0); - var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); - ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize); - - var bytes = new byte[dwSize]; - Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize); - Marshal.FreeHGlobal(unmanagedPointer); - - var lpstr = Encoding.Unicode.GetString(bytes); - io.AddInputCharactersUTF8(lpstr); - - this.ImmComp = string.Empty; - this.ImmCandNative = default; - this.ImmCand.Clear(); - this.ToggleWindow(false); - } - - break; - - default: - break; - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Prevented a crash in an IME hook"); - } - - return null; - } - - /// - /// Get the position of the cursor. - /// - /// The position of the cursor. - internal Vector2 GetCursorPos() - { - return new Vector2(this.cursorPos->X, this.cursorPos->Y); - } - - private unsafe void LoadCand(IntPtr hWnd) - { - if (hWnd == IntPtr.Zero) - return; - - var hImc = ImmGetContext(hWnd); - if (hImc == IntPtr.Zero) - return; - - var size = ImmGetCandidateListW(hImc, 0, IntPtr.Zero, 0); - if (size == 0) - return; - - var candlistPtr = Marshal.AllocHGlobal((int)size); - size = ImmGetCandidateListW(hImc, 0, candlistPtr, (uint)size); - - var candlist = this.ImmCandNative = Marshal.PtrToStructure(candlistPtr); - var pageSize = candlist.PageSize; - var candCount = candlist.Count; - - if (pageSize > 0 && candCount > 1) - { - var dwOffsets = new int[candCount]; - for (var i = 0; i < candCount; i++) - { - dwOffsets[i] = Marshal.ReadInt32(candlistPtr + ((i + 6) * sizeof(int))); - } - - var pageStart = candlist.PageStart; - - var cand = new string[pageSize]; - this.ImmCand.Clear(); - - for (var i = 0; i < pageSize; i++) - { - var offStart = dwOffsets[i + pageStart]; - var offEnd = i + pageStart + 1 < candCount ? dwOffsets[i + pageStart + 1] : size; - - var pStrStart = candlistPtr + (int)offStart; - var pStrEnd = candlistPtr + (int)offEnd; - - var len = (int)(pStrEnd.ToInt64() - pStrStart.ToInt64()); - if (len > 0) - { - var candBytes = new byte[len]; - Marshal.Copy(pStrStart, candBytes, 0, len); - - var candStr = Encoding.Unicode.GetString(candBytes); - cand[i] = candStr; - - this.ImmCand.Add(candStr); - } - } - - Marshal.FreeHGlobal(candlistPtr); - } - } - - [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) - { - try - { - var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "cimgui.dll"); - var scanner = new SigScanner(module); - var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF"); - Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}"); - - this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2)); - this.cursorPos->X = 0f; - this.cursorPos->Y = 0f; - - var asm = new[] - { - "use64", - $"push rax", - $"mov rax, {(IntPtr)this.cursorPos + sizeof(float)}", - $"movss [rax],xmm7", - $"mov rax, {(IntPtr)this.cursorPos}", - $"movss [rax],xmm6", - $"pop rax", - }; - - Log.Debug($"Asm Code:\n{string.Join("\n", asm)}"); - this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook"); - this.imguiTextInputCursorHook?.Enable(); - - this.IsEnabled = true; - Log.Information("Enabled!"); - } - catch (Exception ex) - { - Log.Information(ex, "Enable failed"); - } - } - - private void ToggleWindow(bool visible) - { - if (visible) - Service.GetNullable()?.OpenImeWindow(); - else - Service.GetNullable()?.CloseImeWindow(); - } -} diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs new file mode 100644 index 0000000000..1fc70b0f6d --- /dev/null +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -0,0 +1,521 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Game.Text; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// This class handles IME for non-English users. +/// +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class DalamudIme : IDisposable, IServiceType +{ + private static readonly ModuleLog Log = new("IME"); + + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; + + [ServiceManager.ServiceConstructor] + private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; + + /// + /// Finalizes an instance of the class. + /// + ~DalamudIme() => this.ReleaseUnmanagedResources(); + + private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data); + + /// + /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. + /// + internal static bool ShowCursorInInputText + { + get + { + if (!ImGui.GetIO().ConfigInputTextCursorBlink) + return true; + ref var textState = ref TextState; + if (textState.Id == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0) + return true; + if (textState.CursorAnim <= 0) + return true; + return textState.CursorAnim % 1.2f <= 0.8f; + } + } + + /// + /// Gets the cursor position, in screen coordinates. + /// + internal Vector2 CursorPos { get; private set; } + + /// + /// Gets the associated viewport. + /// + internal ImGuiViewportPtr AssociatedViewport { get; private set; } + + /// + /// Gets the index of the first imm candidate in relation to the full list. + /// + internal CANDIDATELIST ImmCandNative { get; private set; } + + /// + /// Gets the imm candidates. + /// + internal List ImmCand { get; private set; } = new(); + + /// + /// Gets the selected imm component. + /// + internal string ImmComp { get; private set; } = string.Empty; + + /// + /// Gets the partial conversion from-range. + /// + internal int PartialConversionFrom { get; private set; } + + /// + /// Gets the partial conversion to-range. + /// + internal int PartialConversionTo { get; private set; } + + /// + /// Gets the cursor offset in the composition string. + /// + internal int CompositionCursorOffset { get; private set; } + + /// + /// Gets a value indicating whether to display partial conversion status. + /// + internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || + this.PartialConversionTo != this.ImmComp.Length; + + /// + /// Gets the input mode icon from . + /// + internal string? InputModeIcon { get; private set; } + + private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// Processes window messages. + /// + /// The arguments. + public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args) + { + if (!ImGuiHelpers.IsImGuiInitialized) + return; + + // Are we not the target of text input? + if (!ImGui.GetIO().WantTextInput) + return; + + var hImc = ImmGetContext(args.Hwnd); + if (hImc == nint.Zero) + return; + + try + { + var invalidTarget = TextState.Id == 0 || (TextState.Flags & ImGuiInputTextFlags.ReadOnly) != 0; + + switch (args.Message) + { + case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: + this.UpdateImeWindowStatus(hImc); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_STARTCOMPOSITION: + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_COMPOSITION: + if (invalidTarget) + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + else + this.ReplaceCompositionString(hImc, (uint)args.LParam); + + // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(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); + 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); + 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); + break; + + case WM.WM_IME_SETCONTEXT: + // Hide candidate and composition windows. + args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF)); + + // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressWithDefault(); + break; + + case WM.WM_IME_NOTIFY: + // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); + break; + } + + this.UpdateInputLanguage(hImc); + } + finally + { + ImmReleaseContext(args.Hwnd, hImc); + } + } + + private static string ImmGetCompositionString(HIMC hImc, uint comp) + { + var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0); + if (numBytes == 0) + return string.Empty; + + var data = stackalloc char[numBytes / 2]; + _ = ImmGetCompositionStringW(hImc, comp, data, (uint)numBytes); + return new(data, 0, numBytes / 2); + } + + private void ReleaseUnmanagedResources() => ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + + private void UpdateInputLanguage(HIMC hImc) + { + uint conv, sent; + ImmGetConversionStatus(hImc, &conv, &sent); + var lang = GetKeyboardLayout(0); + var open = ImmGetOpenStatus(hImc) != false; + + // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}"); + + var native = (conv & 1) != 0; + var katakana = (conv & 2) != 0; + var fullwidth = (conv & 8) != 0; + switch (lang & 0x3F) + { + case LANG.LANG_KOREAN: + if (native) + this.InputModeIcon = "\uE025"; + else if (fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + else + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + break; + + case LANG.LANG_JAPANESE: + // wtf + // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 + if (open && native && katakana && fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeKatakana}"; + else if (open && native && katakana) + this.InputModeIcon = $"{(char)SeIconChar.ImeKatakanaHalfWidth}"; + else if (open && native) + this.InputModeIcon = $"{(char)SeIconChar.ImeHiragana}"; + else if (open && fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + else + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + break; + + case LANG.LANG_CHINESE: + // TODO: does Chinese IME also need "open" check? + if (native) + this.InputModeIcon = "\uE026"; + else + this.InputModeIcon = "\uE027"; + break; + + default: + this.InputModeIcon = null; + break; + } + + this.UpdateImeWindowStatus(hImc); + } + + private void ReplaceCompositionString(HIMC hImc, uint comp) + { + ref var textState = ref TextState; + var finalCommit = (comp & GCS.GCS_RESULTSTR) != 0; + + ref var s = ref textState.Stb.SelectStart; + ref var e = ref textState.Stb.SelectEnd; + ref var c = ref textState.Stb.Cursor; + s = Math.Clamp(s, 0, textState.CurLenW); + e = Math.Clamp(e, 0, textState.CurLenW); + c = Math.Clamp(c, 0, textState.CurLenW); + if (s == e) + s = e = c; + if (s > e) + (s, e) = (e, s); + + var newString = finalCommit + ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) + : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + + if (s != e) + textState.DeleteChars(s, e - s); + textState.InsertChars(s, newString); + + if (finalCommit) + s = e = s + newString.Length; + else + e = s + newString.Length; + + this.ImmComp = finalCommit ? string.Empty : newString; + + this.CompositionCursorOffset = + finalCommit + ? 0 + : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + + if (finalCommit) + { + this.PartialConversionFrom = this.PartialConversionTo = 0; + } + else if ((comp & GCS.GCS_COMPATTR) != 0) + { + var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); + var attrPtr = stackalloc byte[attrLength]; + var attr = new Span(attrPtr, Math.Min(this.ImmComp.Length, attrLength)); + _ = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, attrPtr, (uint)attrLength); + var l = 0; + while (l < attr.Length && attr[l] is not ATTR_TARGET_CONVERTED and not ATTR_TARGET_NOTCONVERTED) + l++; + + var r = l; + while (r < attr.Length && attr[r] is ATTR_TARGET_CONVERTED or ATTR_TARGET_NOTCONVERTED) + r++; + + if (r == 0 || l == this.ImmComp.Length) + (l, r) = (0, this.ImmComp.Length); + + (this.PartialConversionFrom, this.PartialConversionTo) = (l, r); + } + else + { + this.PartialConversionFrom = 0; + this.PartialConversionTo = this.ImmComp.Length; + } + + // Put the cursor at the beginning, so that the candidate window appears aligned with the text. + c = s; + this.UpdateImeWindowStatus(hImc); + } + + private void ClearState() + { + this.ImmComp = string.Empty; + this.PartialConversionFrom = this.PartialConversionTo = 0; + this.UpdateImeWindowStatus(default); + + ref var textState = ref TextState; + textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; + } + + private void LoadCand(HIMC hImc) + { + this.ImmCand.Clear(); + this.ImmCandNative = default; + + if (hImc == default) + return; + + var size = (int)ImmGetCandidateListW(hImc, 0, null, 0); + if (size == 0) + return; + + var pStorage = stackalloc byte[size]; + if (size != ImmGetCandidateListW(hImc, 0, (CANDIDATELIST*)pStorage, (uint)size)) + return; + + ref var candlist = ref *(CANDIDATELIST*)pStorage; + this.ImmCandNative = candlist; + + if (candlist.dwPageSize == 0 || candlist.dwCount == 0) + return; + + foreach (var i in Enumerable.Range( + (int)candlist.dwPageStart, + (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) + { + this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + } + } + + private void UpdateImeWindowStatus(HIMC hImc) + { + if (Service.GetNullable() is not { } di) + return; + + this.LoadCand(hImc); + if (this.ImmCand.Count != 0 || this.ShowPartialConversion || this.InputModeIcon != default) + di.OpenImeWindow(); + else + di.CloseImeWindow(); + } + + private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) + { + this.CursorPos = data.InputPos; + if (data.WantVisible) + { + this.AssociatedViewport = viewport; + } + else + { + this.AssociatedViewport = default; + this.ClearState(); + } + } + + [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] + private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) => + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + + /// + /// Ported from imstb_textedit.h. + /// + [StructLayout(LayoutKind.Sequential, Size = 0xE2C)] + private struct StbTextEditState + { + /// + /// Position of the text cursor within the string. + /// + public int Cursor; + + /// + /// Selection start point. + /// + public int SelectStart; + + /// + /// selection start and end point in characters; if equal, no selection. + /// + /// + /// Note that start may be less than or greater than end (e.g. when dragging the mouse, + /// start is where the initial click was, and you can drag in either direction.) + /// + public int SelectEnd; + + /// + /// Each text field keeps its own insert mode state. + /// To keep an app-wide insert mode, copy this value in/out of the app state. + /// + public byte InsertMode; + + /// + /// Page size in number of row. + /// This value MUST be set to >0 for pageup or pagedown in multilines documents. + /// + public int RowCountPerPage; + + // Remainder is stb-private data. + } + + [StructLayout(LayoutKind.Sequential)] + private struct ImGuiInputTextState + { + public uint Id; + public int CurLenW; + public int CurLenA; + public ImVector TextWRaw; + public ImVector TextARaw; + public ImVector InitialTextARaw; + public bool TextAIsValid; + public int BufCapacityA; + public float ScrollX; + public StbTextEditState Stb; + public float CursorAnim; + public bool CursorFollow; + public bool SelectedAllMouseLock; + public bool Edited; + public ImGuiInputTextFlags Flags; + + public ImVectorWrapper TextW => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + public ImVectorWrapper TextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + public ImVectorWrapper InitialTextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + // See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS + public void DeleteChars(int pos, int n) + { + var dst = this.TextW.Data + pos; + + // We maintain our buffer length in both UTF-8 and wchar formats + this.Edited = true; + this.CurLenA -= Encoding.UTF8.GetByteCount(dst, n); + this.CurLenW -= n; + + // Offset remaining text (FIXME-OPT: Use memmove) + var src = this.TextW.Data + pos + n; + int i; + for (i = 0; src[i] != 0; i++) + dst[i] = src[i]; + dst[i] = '\0'; + } + + // See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS + public bool InsertChars(int pos, ReadOnlySpan newText) + { + var isResizable = (this.Flags & ImGuiInputTextFlags.CallbackResize) != 0; + var textLen = this.CurLenW; + Debug.Assert(pos <= textLen, "pos <= text_len"); + + var newTextLenUtf8 = Encoding.UTF8.GetByteCount(newText); + if (!isResizable && newTextLenUtf8 + this.CurLenA + 1 > this.BufCapacityA) + return false; + + // Grow internal buffer if needed + if (newText.Length + textLen + 1 > this.TextW.Length) + { + if (!isResizable) + return false; + + Debug.Assert(textLen < this.TextW.Length, "text_len < this.TextW.Length"); + this.TextW.Resize(textLen + Math.Clamp(newText.Length * 4, 32, Math.Max(256, newText.Length)) + 1); + } + + var text = this.TextW.DataSpan; + if (pos != textLen) + text.Slice(pos, textLen - pos).CopyTo(text[(pos + newText.Length)..]); + newText.CopyTo(text[pos..]); + + this.Edited = true; + this.CurLenW += newText.Length; + this.CurLenA += newTextLenUtf8; + this.TextW[this.CurLenW] = '\0'; + + return true; + } + } +} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1dcc5c0c7d..95415659bc 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -59,7 +59,7 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly ComponentDemoWindow componentDemoWindow; private readonly DataWindow dataWindow; private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow; - private readonly ImeWindow imeWindow; + private readonly DalamudImeWindow imeWindow; private readonly ConsoleWindow consoleWindow; private readonly PluginStatWindow pluginStatWindow; private readonly PluginInstallerWindow pluginWindow; @@ -111,7 +111,7 @@ private DalamudInterface( this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; - this.imeWindow = new ImeWindow() { IsOpen = false }; + this.imeWindow = new DalamudImeWindow() { IsOpen = false }; this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; @@ -256,7 +256,7 @@ public void OpenDataWindow(string? dataKind = null) public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true; /// - /// Opens the . + /// Opens the . /// public void OpenImeWindow() => this.imeWindow.IsOpen = true; @@ -356,7 +356,7 @@ public void OpenBranchSwitcher() #region Close /// - /// Closes the . + /// Closes the . /// public void CloseImeWindow() => this.imeWindow.IsOpen = false; @@ -408,7 +408,7 @@ public void ToggleDataWindow(string? dataKind = null) public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle(); /// - /// Toggles the . + /// Toggles the . /// public void ToggleImeWindow() => this.imeWindow.Toggle(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1b12fd853a..d7ab5ba9d9 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -12,7 +12,6 @@ using Dalamud.Game; using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Gui.Internal; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Interface.GameFonts; @@ -73,12 +72,16 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly WndProcHookManager wndProcHookManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudIme dalamudIme = Service.Get(); private readonly ManualResetEvent fontBuildSignal; private readonly SwapChainVtableResolver address; - private readonly Hook dispatchMessageWHook; private readonly Hook setCursorHook; - private Hook processMessageHook; private RawDX11Scene? scene; private Hook? presentHook; @@ -92,8 +95,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceConstructor] private InterfaceManager() { - this.dispatchMessageWHook = Hook.FromImport( - null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); @@ -111,12 +112,6 @@ private InterfaceManager() [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr SetCursorDelegate(IntPtr hCursor); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate IntPtr DispatchMessageWDelegate(ref User32.MSG msg); - - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ProcessMessageDelegate(IntPtr hWnd, uint msg, ulong wParam, ulong lParam, IntPtr handeled); - /// /// This event gets called each frame to facilitate ImGui drawing. /// @@ -236,10 +231,9 @@ public void Dispose() this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - this.dispatchMessageWHook.Dispose(); - this.processMessageHook?.Dispose(); }).Wait(); + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; this.scene?.Dispose(); } @@ -660,6 +654,20 @@ private void InitScene(IntPtr swapChain) this.scene = newScene; Service.Provide(new(this)); + + this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; + } + + private unsafe void WndProcHookManagerOnPreWndProc(ref WndProcHookManager.WndProcOverrideEventArgs 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; + } + + this.dalamudIme.ProcessImeMessage(ref args); } /* @@ -1095,15 +1103,9 @@ private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfigurat Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - var wndProcAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8"); - Log.Verbose($"WndProc address 0x{wndProcAddress.ToInt64():X}"); - this.processMessageHook = Hook.FromAddress(wndProcAddress, this.ProcessMessageDetour); - this.setCursorHook.Enable(); this.presentHook.Enable(); this.resizeBuffersHook.Enable(); - this.dispatchMessageWHook.Enable(); - this.processMessageHook.Enable(); }); } @@ -1124,25 +1126,6 @@ private void RebuildFontsInternal() this.isRebuildingFonts = false; } - private unsafe IntPtr ProcessMessageDetour(IntPtr hWnd, uint msg, ulong wParam, ulong lParam, IntPtr handeled) - { - var ime = Service.GetNullable(); - var res = ime?.ProcessWndProcW(hWnd, (User32.WindowMessage)msg, (void*)wParam, (void*)lParam); - return this.processMessageHook.Original(hWnd, msg, wParam, lParam, handeled); - } - - private unsafe IntPtr DispatchMessageWDetour(ref User32.MSG msg) - { - if (msg.hwnd == this.GameWindowHandle && this.scene != null) - { - var res = this.scene.ProcessWndProcW(msg.hwnd, msg.message, (void*)msg.wParam, (void*)msg.lParam); - if (res != null) - return res.Value; - } - - return this.dispatchMessageWHook.IsDisposed ? User32.DispatchMessage(ref msg) : this.dispatchMessageWHook.Original(ref msg); - } - private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) { #if DEBUG diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs new file mode 100644 index 0000000000..1819ed8193 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -0,0 +1,223 @@ +using System.Numerics; + +using Dalamud.Interface.Windowing; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows; + +/// +/// A window for displaying IME details. +/// +internal unsafe class DalamudImeWindow : Window +{ + private const int ImePageSize = 9; + + /// + /// Initializes a new instance of the class. + /// + public DalamudImeWindow() + : base( + "Dalamud IME", + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoBackground) + { + this.Size = default(Vector2); + + this.RespectCloseHotkey = false; + } + + /// + public override void Draw() + { + } + + /// + public override void PostDraw() + { + if (Service.GetNullable() is not { } ime) + return; + + var viewport = ime.AssociatedViewport; + if (viewport.NativePtr is null) + return; + + var drawCand = ime.ImmCand.Count != 0; + var drawConv = drawCand || ime.ShowPartialConversion; + var drawIme = ime.InputModeIcon != null; + + var pad = ImGui.GetStyle().WindowPadding; + var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp); + + var native = ime.ImmCandNative; + var totalIndex = native.dwSelection + 1; + var totalSize = native.dwCount; + + var pageStart = native.dwPageStart; + var pageIndex = (pageStart / ImePageSize) + 1; + var pageCount = (totalSize / ImePageSize) + 1; + var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; + + // Calc the window size. + var maxTextWidth = 0f; + for (var i = 0; i < ime.ImmCand.Count; i++) + { + var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}"); + maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; + } + + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X + ? maxTextWidth + : ImGui.CalcTextSize(ime.ImmComp).X; + + var numEntries = (drawCand ? ime.ImmCand.Count + 1 : 0) + 1 + (drawIme ? 1 : 0); + var spaceY = ImGui.GetStyle().ItemSpacing.Y; + var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries); + var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2); + + // 1. Figure out the expanding direction. + var expandUpward = ime.CursorPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y; + var windowPos = ime.CursorPos - pad; + if (expandUpward) + { + windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2); + if (drawIme) + windowPos.Y += candTextSize.Y + spaceY; + } + else + { + if (drawIme) + windowPos.Y -= candTextSize.Y + spaceY; + } + + // 2. Contain within the viewport. Do not use clamp, as the target window might be too small. + if (windowPos.X < viewport.WorkPos.X) + windowPos.X = viewport.WorkPos.X; + else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X) + windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X; + if (windowPos.Y < viewport.WorkPos.Y) + windowPos.Y = viewport.WorkPos.Y; + else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y) + windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y; + + var cursor = windowPos + pad; + + // Draw the ime window. + var drawList = ImGui.GetForegroundDrawList(viewport); + + // Draw the background rect for candidates. + if (drawCand) + { + Vector2 candRectLt, candRectRb; + if (!expandUpward) + { + candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 }; + candRectRb = windowPos + windowSize; + if (drawIme) + candRectLt.Y += spaceY + candTextSize.Y; + } + else + { + candRectLt = windowPos; + candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 }); + if (drawIme) + candRectRb.Y -= spaceY + candTextSize.Y; + } + + drawList.AddRectFilled( + candRectLt, + candRectRb, + ImGui.GetColorU32(ImGuiCol.WindowBg), + ImGui.GetStyle().WindowRounding); + } + + if (!expandUpward && drawIme) + { + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + cursor.Y += candTextSize.Y + spaceY; + } + + if (!expandUpward && drawConv) + { + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + } + + if (drawCand) + { + // Add the candidate words. + for (var i = 0; i < ime.ImmCand.Count; i++) + { + var selected = i == (native.dwSelection % ImePageSize); + var color = ImGui.GetColorU32(ImGuiCol.Text); + if (selected) + color = ImGui.GetColorU32(ImGuiCol.NavHighlight); + + drawList.AddText(cursor, color, $"{i + 1}. {ime.ImmCand[i]}"); + cursor.Y += candTextSize.Y + spaceY; + } + + // Add a separator + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + // Add the pages infomation. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawConv) + { + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawIme) + { + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + } + + return; + + void DrawTextBeingConverted() + { + // Draw the text background. + drawList.AddRectFilled( + cursor - (pad / 2), + cursor + candTextSize + (pad / 2), + ImGui.GetColorU32(ImGuiCol.WindowBg)); + + // If only a part of the full text is marked for conversion, then draw background for the part being edited. + if (ime.PartialConversionFrom != 0 || ime.PartialConversionTo != ime.ImmComp.Length) + { + var part1 = ime.ImmComp[..ime.PartialConversionFrom]; + var part2 = ime.ImmComp[..ime.PartialConversionTo]; + var size1 = ImGui.CalcTextSize(part1); + var size2 = ImGui.CalcTextSize(part2); + drawList.AddRectFilled( + cursor + size1 with { Y = 0 }, + cursor + size2, + ImGui.GetColorU32(ImGuiCol.TextSelectedBg)); + } + + // Add the text being converted. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp); + + // Draw the caret inside the composition string. + if (DalamudIme.ShowCursorInInputText) + { + var partBeforeCaret = ime.ImmComp[..ime.CompositionCursorOffset]; + var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret); + drawList.AddLine( + cursor + sizeBeforeCaret with { Y = 0 }, + cursor + sizeBeforeCaret, + ImGui.GetColorU32(ImGuiCol.Text)); + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/IMEWindow.cs b/Dalamud/Interface/Internal/Windows/IMEWindow.cs deleted file mode 100644 index 80e03caf33..0000000000 --- a/Dalamud/Interface/Internal/Windows/IMEWindow.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Numerics; - -using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Gui.Internal; -using Dalamud.Interface.Windowing; -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Windows; - -/// -/// A window for displaying IME details. -/// -internal unsafe class ImeWindow : Window -{ - private const int ImePageSize = 9; - - /// - /// Initializes a new instance of the class. - /// - public ImeWindow() - : base("Dalamud IME", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground) - { - this.Size = new Vector2(100, 200); - this.SizeCondition = ImGuiCond.FirstUseEver; - - this.RespectCloseHotkey = false; - } - - /// - public override void Draw() - { - if (this.IsOpen && Service.Get()[VirtualKey.SHIFT]) Service.Get().CloseImeWindow(); - var ime = Service.GetNullable(); - - if (ime == null || !ime.IsEnabled) - { - ImGui.Text("IME is unavailable."); - return; - } - - // ImGui.Text($"{ime.GetCursorPos()}"); - // ImGui.Text($"{ImGui.GetWindowViewport().WorkSize}"); - } - - /// - public override void PostDraw() - { - if (this.IsOpen && Service.Get()[VirtualKey.SHIFT]) Service.Get().CloseImeWindow(); - var ime = Service.GetNullable(); - - if (ime == null || !ime.IsEnabled) - return; - - var maxTextWidth = 0f; - var textHeight = ImGui.CalcTextSize(ime.ImmComp).Y; - - var native = ime.ImmCandNative; - var totalIndex = native.Selection + 1; - var totalSize = native.Count; - - var pageStart = native.PageStart; - var pageIndex = (pageStart / ImePageSize) + 1; - var pageCount = (totalSize / ImePageSize) + 1; - var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; - - // Calc the window size - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}"); - maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; - } - - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X ? maxTextWidth : ImGui.CalcTextSize(ime.ImmComp).X; - - var imeWindowWidth = maxTextWidth + (2 * ImGui.GetStyle().WindowPadding.X); - var imeWindowHeight = (textHeight * (ime.ImmCand.Count + 2)) + (5 * (ime.ImmCand.Count - 1)) + (2 * ImGui.GetStyle().WindowPadding.Y); - - // Calc the window pos - var cursorPos = ime.GetCursorPos(); - var imeWindowMinPos = new Vector2(cursorPos.X, cursorPos.Y); - var imeWindowMaxPos = new Vector2(imeWindowMinPos.X + imeWindowWidth, imeWindowMinPos.Y + imeWindowHeight); - var gameWindowSize = ImGui.GetWindowViewport().WorkSize; - - var offset = new Vector2( - imeWindowMaxPos.X - gameWindowSize.X > 0 ? imeWindowMaxPos.X - gameWindowSize.X : 0, - imeWindowMaxPos.Y - gameWindowSize.Y > 0 ? imeWindowMaxPos.Y - gameWindowSize.Y : 0); - imeWindowMinPos -= offset; - imeWindowMaxPos -= offset; - - var nextDrawPosY = imeWindowMinPos.Y; - var drawAreaPosX = imeWindowMinPos.X + ImGui.GetStyle().WindowPadding.X; - - // Draw the ime window - var drawList = ImGui.GetForegroundDrawList(); - // Draw the background rect - drawList.AddRectFilled(imeWindowMinPos, imeWindowMaxPos, ImGui.GetColorU32(ImGuiCol.WindowBg), ImGui.GetStyle().WindowRounding); - // Add component text - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp); - nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y; - // Add separator - drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator)); - // Add candidate words - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var selected = i == (native.Selection % ImePageSize); - var color = ImGui.GetColorU32(ImGuiCol.Text); - if (selected) - color = ImGui.GetColorU32(ImGuiCol.NavHighlight); - - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), color, $"{i + 1}. {ime.ImmCand[i]}"); - nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y; - } - - // Add separator - drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator)); - // Add pages infomation - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), pageInfo); - } -} diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs new file mode 100644 index 0000000000..fcd90c95a7 --- /dev/null +++ b/Dalamud/Interface/Internal/WndProcHookManager.cs @@ -0,0 +1,273 @@ +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) + { + // Remove self from the chain. + SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); + lock (this.wndProcNextDict) + this.wndProcNextDict.Remove(hwnd); + + // Even though this message is dedicated for our processing, + // satisfy the expectations by calling the next window procedure. + return CallWindowProcW( + (delegate* unmanaged)nextProc, + hwnd, + uMsg, + wParam, + lParam); + } + + 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; + } + } +} diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 579d93f867..85f81b2037 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -426,6 +426,26 @@ public static void CenterCursorFor(float itemWidth) => /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// Finds the corresponding ImGui viewport ID for the given window handle. + /// + /// The window handle. + /// The viewport ID, or -1 if not found. + internal static unsafe int FindViewportId(nint hwnd) + { + if (!IsImGuiInitialized) + return -1; + + var viewports = new ImVectorWrapper(&ImGui.GetPlatformIO().NativePtr->Viewports); + for (var i = 0; i < viewports.LengthUnsafe; i++) + { + if (viewports.DataUnsafe[i].PlatformHandle == hwnd) + return i; + } + + return -1; + } /// /// Get data needed for each new frame. From e089949a728893474d83d05393df5487ace96c0d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:33:18 +0900 Subject: [PATCH 02/10] fix minor things --- Dalamud/Interface/Internal/DalamudIme.cs | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 1fc70b0f6d..6535228a7f 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -184,6 +184,13 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar case WM.WM_IME_NOTIFY: // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); break; + + case WM.WM_LBUTTONDOWN: + case WM.WM_RBUTTONDOWN: + case WM.WM_MBUTTONDOWN: + case WM.WM_XBUTTONDOWN: + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); + break; } this.UpdateInputLanguage(hImc); @@ -299,9 +306,11 @@ private void ReplaceCompositionString(HIMC hImc, uint comp) if (finalCommit) { - this.PartialConversionFrom = this.PartialConversionTo = 0; + this.ClearState(hImc); + return; } - else if ((comp & GCS.GCS_COMPATTR) != 0) + + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); var attrPtr = stackalloc byte[attrLength]; @@ -331,14 +340,17 @@ private void ReplaceCompositionString(HIMC hImc, uint comp) this.UpdateImeWindowStatus(hImc); } - private void ClearState() + private void ClearState(HIMC hImc) { this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; + this.CompositionCursorOffset = 0; this.UpdateImeWindowStatus(default); ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; + + Log.Information($"{nameof(this.ClearState)}"); } private void LoadCand(HIMC hImc) @@ -386,15 +398,7 @@ private void UpdateImeWindowStatus(HIMC hImc) private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) { this.CursorPos = data.InputPos; - if (data.WantVisible) - { - this.AssociatedViewport = viewport; - } - else - { - this.AssociatedViewport = default; - this.ClearState(); - } + this.AssociatedViewport = data.WantVisible ? viewport : default; } [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] From f03552a2ab51aa3a5c1b24501028beb5127db354 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:39:11 +0900 Subject: [PATCH 03/10] Prevent Tab key from breaking input --- Dalamud/Interface/Internal/DalamudIme.cs | 54 +++++++++++++++++------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 6535228a7f..718ec53e69 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -55,12 +55,12 @@ internal static bool ShowCursorInInputText return textState.CursorAnim % 1.2f <= 0.8f; } } - + /// /// Gets the cursor position, in screen coordinates. /// internal Vector2 CursorPos { get; private set; } - + /// /// Gets the associated viewport. /// @@ -101,7 +101,7 @@ internal static bool ShowCursorInInputText /// internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || this.PartialConversionTo != this.ImmComp.Length; - + /// /// Gets the input mode icon from . /// @@ -139,15 +139,17 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar switch (args.Message) { - case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: + case WM.WM_IME_NOTIFY + when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE + or IMN.IMN_CHANGECANDIDATE: this.UpdateImeWindowStatus(hImc); args.SuppressAndReturn(0); break; - + case WM.WM_IME_STARTCOMPOSITION: args.SuppressAndReturn(0); break; - + case WM.WM_IME_COMPOSITION: if (invalidTarget) ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); @@ -162,12 +164,12 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressAndReturn(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); 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); @@ -180,11 +182,31 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressWithDefault(); break; - + case WM.WM_IME_NOTIFY: // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); break; + case WM.WM_KEYDOWN when (int)args.WParam is + VK.VK_TAB + or VK.VK_PRIOR + or VK.VK_NEXT + or VK.VK_END + or VK.VK_HOME + or VK.VK_LEFT + or VK.VK_UP + or VK.VK_RIGHT + or VK.VK_DOWN + or VK.VK_RETURN: + if (this.ImmCand.Count != 0) + { + TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + args.WParam = VK.VK_PROCESSKEY; + } + + break; + case WM.WM_LBUTTONDOWN: case WM.WM_RBUTTONDOWN: case WM.WM_MBUTTONDOWN: @@ -192,7 +214,7 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); break; } - + this.UpdateInputLanguage(hImc); } finally @@ -220,7 +242,7 @@ private void UpdateInputLanguage(HIMC hImc) ImmGetConversionStatus(hImc, &conv, &sent); var lang = GetKeyboardLayout(0); var open = ImmGetOpenStatus(hImc) != false; - + // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}"); var native = (conv & 1) != 0; @@ -285,8 +307,8 @@ private void ReplaceCompositionString(HIMC hImc, uint comp) (s, e) = (e, s); var newString = finalCommit - ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) - : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) + : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); if (s != e) textState.DeleteChars(s, e - s); @@ -303,13 +325,13 @@ private void ReplaceCompositionString(HIMC hImc, uint comp) finalCommit ? 0 : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); - + if (finalCommit) { this.ClearState(hImc); return; } - + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); @@ -349,7 +371,7 @@ private void ClearState(HIMC hImc) ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - + Log.Information($"{nameof(this.ClearState)}"); } From 01b45c98ac0abc12a6c644d61a1a7f82525a5e8e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:42:54 +0900 Subject: [PATCH 04/10] w --- Dalamud/Interface/Internal/DalamudIme.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 718ec53e69..2861975903 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -200,8 +200,7 @@ or VK.VK_DOWN or VK.VK_RETURN: if (this.ImmCand.Count != 0) { - TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; - ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + this.ClearState(hImc); args.WParam = VK.VK_PROCESSKEY; } @@ -367,6 +366,8 @@ private void ClearState(HIMC hImc) this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; this.CompositionCursorOffset = 0; + TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); this.UpdateImeWindowStatus(default); ref var textState = ref TextState; From 2c3139d8b7685d16b1db1268361a0176aab6efeb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 15:41:37 +0900 Subject: [PATCH 05/10] Ensure borders on IME mode foreground icon --- Dalamud/Game/Text/SeIconChar.cs | 32 ++++++++++++++++--- Dalamud/Interface/Internal/DalamudIme.cs | 9 +++--- .../Internal/Windows/DalamudImeWindow.cs | 28 ++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Text/SeIconChar.cs b/Dalamud/Game/Text/SeIconChar.cs index c1be006131..17924c6710 100644 --- a/Dalamud/Game/Text/SeIconChar.cs +++ b/Dalamud/Game/Text/SeIconChar.cs @@ -611,29 +611,51 @@ public enum SeIconChar QuestRepeatable = 0xE0BF, /// - /// The IME hiragana icon unicode character. + /// The [あ] character indicating that the Japanese IME is in full-width Hiragana input mode. /// + /// + /// Half-width Hiragana exists as a Windows API constant, but the feature is unused, or at least unexposed to the end user via the IME. + /// ImeHiragana = 0xE020, /// - /// The IME katakana icon unicode character. + /// The [ア] character indicating that the Japanese IME is in full-width Katakana input mode. /// ImeKatakana = 0xE021, /// - /// The IME alphanumeric icon unicode character. + /// The [A] character indicating that Japanese or Korean IME is in full-width Latin character input mode. /// ImeAlphanumeric = 0xE022, /// - /// The IME katakana half-width icon unicode character. + /// The [_ア] character indicating that the Japanese IME is in half-width Katakana input mode. /// ImeKatakanaHalfWidth = 0xE023, /// - /// The IME alphanumeric half-width icon unicode character. + /// The [_A] character indicating that Japanese or Korean IME is in half-width Latin character input mode. /// ImeAlphanumericHalfWidth = 0xE024, + + /// + /// The [가] character indicating that the Korean IME is in Hangul input mode. + /// + /// + /// Use and for alphanumeric input mode, + /// toggled via Alt+=. + /// + ImeKoreanHangul = 0xE025, + + /// + /// The [中] character indicating that the Chinese IME is in Han character input mode. + /// + ImeChineseHan = 0xE026, + + /// + /// The [英] character indicating that the Chinese IME is in Latin character input mode. + /// + ImeChineseLatin = 0xE027, /// /// The instance (1) icon unicode character. diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 2861975903..9e0466e661 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -19,7 +19,7 @@ namespace Dalamud.Interface.Internal; /// -/// This class handles IME for non-English users. +/// This class handles CJK IME. /// [ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType @@ -251,7 +251,7 @@ private void UpdateInputLanguage(HIMC hImc) { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = "\uE025"; + this.InputModeIcon = $"{(char)SeIconChar.ImeKoreanHangul}"; else if (fullwidth) this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; else @@ -274,11 +274,10 @@ private void UpdateInputLanguage(HIMC hImc) break; case LANG.LANG_CHINESE: - // TODO: does Chinese IME also need "open" check? if (native) - this.InputModeIcon = "\uE026"; + this.InputModeIcon = $"{(char)SeIconChar.ImeChineseHan}"; else - this.InputModeIcon = "\uE027"; + this.InputModeIcon = $"{(char)SeIconChar.ImeChineseLatin}"; break; default: diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs index 1819ed8193..7417afd911 100644 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -133,6 +133,20 @@ public override void PostDraw() if (!expandUpward && drawIme) { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + drawList.AddText( + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.InputModeIcon); + } + } + } + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); cursor.Y += candTextSize.Y + spaceY; } @@ -179,6 +193,20 @@ public override void PostDraw() if (expandUpward && drawIme) { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + drawList.AddText( + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.InputModeIcon); + } + } + } + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); } From 806ecc0faf7b019d3a31093561778c2f4328fb0e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 15:48:20 +0900 Subject: [PATCH 06/10] Use RenderChar instead of AddText --- Dalamud/Interface/Internal/DalamudIme.cs | 24 +++++++++--------- .../Internal/Windows/DalamudImeWindow.cs | 25 +++++++++++++++---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 9e0466e661..f44c885cee 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -105,7 +105,7 @@ internal static bool ShowCursorInInputText /// /// Gets the input mode icon from . /// - internal string? InputModeIcon { get; private set; } + internal char InputModeIcon { get; private set; } private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); @@ -251,37 +251,37 @@ private void UpdateInputLanguage(HIMC hImc) { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = $"{(char)SeIconChar.ImeKoreanHangul}"; + this.InputModeIcon = (char)SeIconChar.ImeKoreanHangul; else if (fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_JAPANESE: // wtf // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 if (open && native && katakana && fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeKatakana}"; + this.InputModeIcon = (char)SeIconChar.ImeKatakana; else if (open && native && katakana) - this.InputModeIcon = $"{(char)SeIconChar.ImeKatakanaHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth; else if (open && native) - this.InputModeIcon = $"{(char)SeIconChar.ImeHiragana}"; + this.InputModeIcon = (char)SeIconChar.ImeHiragana; else if (open && fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_CHINESE: if (native) - this.InputModeIcon = $"{(char)SeIconChar.ImeChineseHan}"; + this.InputModeIcon = (char)SeIconChar.ImeChineseHan; else - this.InputModeIcon = $"{(char)SeIconChar.ImeChineseLatin}"; + this.InputModeIcon = (char)SeIconChar.ImeChineseLatin; break; default: - this.InputModeIcon = null; + this.InputModeIcon = default; break; } diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs index 7417afd911..ecaa522e5d 100644 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -43,7 +43,8 @@ public override void PostDraw() var drawCand = ime.ImmCand.Count != 0; var drawConv = drawCand || ime.ShowPartialConversion; - var drawIme = ime.InputModeIcon != null; + var drawIme = ime.InputModeIcon != 0; + var imeIconFont = InterfaceManager.DefaultFont; var pad = ImGui.GetStyle().WindowPadding; var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp); @@ -139,7 +140,9 @@ public override void PostDraw() { if (dx != 0 || dy != 0) { - drawList.AddText( + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, cursor + new Vector2(dx, dy), ImGui.GetColorU32(ImGuiCol.WindowBg), ime.InputModeIcon); @@ -147,7 +150,12 @@ public override void PostDraw() } } - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.InputModeIcon); cursor.Y += candTextSize.Y + spaceY; } @@ -199,7 +207,9 @@ public override void PostDraw() { if (dx != 0 || dy != 0) { - drawList.AddText( + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, cursor + new Vector2(dx, dy), ImGui.GetColorU32(ImGuiCol.WindowBg), ime.InputModeIcon); @@ -207,7 +217,12 @@ public override void PostDraw() } } - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.InputModeIcon); } return; From b910ebc014f4ac7fdefa0839d574c0626fde794c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 22:32:28 +0900 Subject: [PATCH 07/10] Auto-enable fonts depending on the character input --- Dalamud/Interface/Internal/DalamudIme.cs | 68 +++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 56 ++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index f44c885cee..f8d7fb690b 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -5,8 +5,10 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Text.Unicode; using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -26,6 +28,26 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { private static readonly ModuleLog Log = new("IME"); + private static readonly UnicodeRange[] HanRange = + { + UnicodeRanges.CjkRadicalsSupplement, + UnicodeRanges.CjkSymbolsandPunctuation, + UnicodeRanges.CjkUnifiedIdeographsExtensionA, + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkCompatibilityIdeographs, + UnicodeRanges.CjkCompatibilityForms, + // No more; Extension B~ are outside BMP range + }; + + private static readonly UnicodeRange[] HangulRange = + { + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB, + }; + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; [ServiceManager.ServiceConstructor] @@ -38,6 +60,16 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data); + /// + /// Gets a value indicating whether Han(Chinese) input has been detected. + /// + public bool EncounteredHan { get; private set; } + + /// + /// Gets a value indicating whether Hangul(Korean) input has been detected. + /// + public bool EncounteredHangul { get; private set; } + /// /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. /// @@ -116,6 +148,39 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Looks for the characters inside and enables fonts accordingly. + /// + /// The string. + public void ReflectCharacterEncounters(string str) + { + foreach (var chr in str) + { + if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + { + if (Service.Get() + .GetFdtReader(GameFontFamilyAndSize.Axis12) + ?.FindGlyph(chr) is null) + { + if (!this.EncounteredHan) + { + this.EncounteredHan = true; + Service.Get().RebuildFonts(); + } + } + } + + if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + { + if (!this.EncounteredHangul) + { + this.EncounteredHangul = true; + Service.Get().RebuildFonts(); + } + } + } + } + /// /// Processes window messages. /// @@ -308,6 +373,8 @@ private void ReplaceCompositionString(HIMC hImc, uint comp) ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + this.ReflectCharacterEncounters(newString); + if (s != e) textState.DeleteChars(s, e - s); textState.InsertChars(s, newString); @@ -402,6 +469,7 @@ private void LoadCand(HIMC hImc) (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) { this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + this.ReflectCharacterEncounters(this.ImmCand[^1]); } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d7ab5ba9d9..49dfdb248a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Text.Unicode; using System.Threading; using Dalamud.Configuration.Internal; @@ -786,10 +787,22 @@ private unsafe void SetupFonts() var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); if (!File.Exists(fontPathKr)) fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); if (!File.Exists(fontPathKr)) fontPathKr = null; Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); + var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); + + var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); + // Default font Log.Verbose("[FONT] SetupFonts - Default font"); var fontInfo = new TargetFontModification( @@ -817,7 +830,8 @@ private unsafe void SetupFonts() this.loadedFontInfo[DefaultFont] = fontInfo; } - if (fontPathKr != null && Service.Get().EffectiveLanguage == "ko") + if (fontPathKr != null + && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) { fontConfig.MergeMode = true; fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); @@ -826,6 +840,46 @@ private unsafe void SetupFonts() fontConfig.MergeMode = false; } + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || this.dalamudIme.EncounteredHan)) + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + // FontAwesome icon font Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); { From 4be635be675204da1c0c59c6eb04e08e112754ce Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 12:40:33 +0900 Subject: [PATCH 08/10] Remove ClearState log --- Dalamud/Interface/Internal/DalamudIme.cs | 2 +- Dalamud/Interface/Internal/WndProcHookManager.cs | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index f8d7fb690b..b3252546a3 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -439,7 +439,7 @@ private void ClearState(HIMC hImc) ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - Log.Information($"{nameof(this.ClearState)}"); + // Log.Information($"{nameof(this.ClearState)}"); } private void LoadCand(HIMC hImc) diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs index fcd90c95a7..1110ff3876 100644 --- a/Dalamud/Interface/Internal/WndProcHookManager.cs +++ b/Dalamud/Interface/Internal/WndProcHookManager.cs @@ -112,19 +112,21 @@ private unsafe LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM if (uMsg == this.unhookSelfMessage) { - // Remove self from the chain. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - // Even though this message is dedicated for our processing, // satisfy the expectations by calling the next window procedure. - return CallWindowProcW( + 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); From 0afb3d2c8af5cb538fd88f0d9d113ee21ff036f6 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 13:33:40 +0900 Subject: [PATCH 09/10] Better WndProc handling --- .../Hooking/WndProcHook/WndProcEventArgs.cs | 144 +++++++++ .../WndProcHook/WndProcEventDelegate.cs | 7 + .../Hooking/WndProcHook/WndProcHookManager.cs | 115 ++++++++ Dalamud/Interface/Internal/DalamudIme.cs | 33 ++- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Interface/Internal/WndProcHookManager.cs | 275 ------------------ 6 files changed, 294 insertions(+), 290 deletions(-) create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcHookManager.cs delete mode 100644 Dalamud/Interface/Internal/WndProcHookManager.cs diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs new file mode 100644 index 0000000000..b25df5d147 --- /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 0000000000..f753f16cc2 --- /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 0000000000..00934f27f0 --- /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 b3252546a3..9bd9a24983 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -8,6 +8,7 @@ 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 static bool ShowCursorInInputText { get { + if (!ImGuiHelpers.IsImGuiInitialized) + return true; if (!ImGui.GetIO().ConfigInputTextCursorBlink) return true; ref var textState = ref TextState; @@ -185,7 +188,7 @@ public void ReflectCharacterEncounters(string str) /// 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 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar 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 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar 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 @@ private static string ImmGetCompositionString(HIMC hImc, uint comp) 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 @@ private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformIme } [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 49dfdb248a..48157fa866 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -15,6 +15,7 @@ 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 @@ private void InitScene(IntPtr swapChain) 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 1110ff3876..0000000000 --- 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; - } - } -} From 6fefc3bee0692310ed14babecba948e3f9777c69 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 14:09:38 +0900 Subject: [PATCH 10/10] Safer unload --- .../Hooking/WndProcHook/WndProcHookManager.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs index 00934f27f0..91020f8980 100644 --- a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -21,6 +22,8 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable private readonly Hook dispatchMessageWHook; private readonly Dictionary wndProcOverrides = new(); + private HWND mainWindowHwnd; + [ServiceManager.ServiceConstructor] private unsafe WndProcHookManager() { @@ -31,6 +34,12 @@ private unsafe WndProcHookManager() 0, this.DispatchMessageWDetour); this.dispatchMessageWHook.Enable(); + + // Capture the game main window handle, + // so that no guarantees would have to be made on the service dispose order. + Service + .GetAsync() + .ContinueWith(r => this.mainWindowHwnd = (HWND)r.Result.Manager.GameWindowHandle); } [UnmanagedFunctionPointer(CallingConvention.StdCall)] @@ -49,7 +58,19 @@ private unsafe WndProcHookManager() /// public void Dispose() { + if (this.dispatchMessageWHook.IsDisposed) + return; + this.dispatchMessageWHook.Dispose(); + + // Ensure that either we're on the main thread, or DispatchMessage is executed at least once. + // The game calls DispatchMessageW only from its main thread, so if we're already on one, + // this line does nothing; if not, it will require a cycle of GetMessage ... DispatchMessageW, + // which at the point of returning from DispatchMessageW(=point of returning from SendMessageW), + // the hook would be guaranteed to be fully disabled and detour delegate would be safe to be released. + SendMessageW(this.mainWindowHwnd, WM.WM_NULL, 0, 0); + + // Now this.wndProcOverrides cannot be touched from other thread. foreach (var v in this.wndProcOverrides.Values) v.InternalRelease(); this.wndProcOverrides.Clear();