mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-15 13:14:17 +01:00
Make CJK imes work better
This commit is contained in:
parent
280a9d6b05
commit
b6d88f798a
9 changed files with 1064 additions and 466 deletions
|
|
@ -9,7 +9,6 @@ using System.Threading.Tasks;
|
|||
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 @@ internal sealed class Dalamud : IServiceType
|
|||
// 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<DalamudIME>.GetNullable()?.Dispose();
|
||||
Service<DalamudIme>.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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class handles IME for non-English users.
|
||||
/// </summary>
|
||||
[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()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the module is enabled.
|
||||
/// </summary>
|
||||
internal bool IsEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the first imm candidate in relation to the full list.
|
||||
/// </summary>
|
||||
internal CandidateList ImmCandNative { get; private set; } = default;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the imm candidates.
|
||||
/// </summary>
|
||||
internal List<string> ImmCand { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected imm component.
|
||||
/// </summary>
|
||||
internal string ImmComp { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.imguiTextInputCursorHook?.Dispose();
|
||||
Marshal.FreeHGlobal((IntPtr)this.cursorPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes window messages.
|
||||
/// </summary>
|
||||
/// <param name="hWnd">Handle of the window.</param>
|
||||
/// <param name="msg">Type of window message.</param>
|
||||
/// <param name="wParamPtr">wParam or the pointer to it.</param>
|
||||
/// <param name="lParamPtr">lParam or the pointer to it.</param>
|
||||
/// <returns>Return value, if not doing further processing.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the position of the cursor.
|
||||
/// </summary>
|
||||
/// <returns>The position of the cursor.</returns>
|
||||
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<CandidateList>(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<ProcessModule>().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<DalamudInterface>.GetNullable()?.OpenImeWindow();
|
||||
else
|
||||
Service<DalamudInterface>.GetNullable()?.CloseImeWindow();
|
||||
}
|
||||
}
|
||||
521
Dalamud/Interface/Internal/DalamudIme.cs
Normal file
521
Dalamud/Interface/Internal/DalamudIme.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class handles IME for non-English users.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="DalamudIme"/> class.
|
||||
/// </summary>
|
||||
~DalamudIme() => this.ReleaseUnmanagedResources();
|
||||
|
||||
private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to display the cursor in input text. This also deals with blinking.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cursor position, in screen coordinates.
|
||||
/// </summary>
|
||||
internal Vector2 CursorPos { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated viewport.
|
||||
/// </summary>
|
||||
internal ImGuiViewportPtr AssociatedViewport { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the first imm candidate in relation to the full list.
|
||||
/// </summary>
|
||||
internal CANDIDATELIST ImmCandNative { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the imm candidates.
|
||||
/// </summary>
|
||||
internal List<string> ImmCand { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected imm component.
|
||||
/// </summary>
|
||||
internal string ImmComp { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the partial conversion from-range.
|
||||
/// </summary>
|
||||
internal int PartialConversionFrom { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the partial conversion to-range.
|
||||
/// </summary>
|
||||
internal int PartialConversionTo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cursor offset in the composition string.
|
||||
/// </summary>
|
||||
internal int CompositionCursorOffset { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to display partial conversion status.
|
||||
/// </summary>
|
||||
internal bool ShowPartialConversion => this.PartialConversionFrom != 0 ||
|
||||
this.PartialConversionTo != this.ImmComp.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input mode icon from <see cref="SeIconChar"/>.
|
||||
/// </summary>
|
||||
internal string? InputModeIcon { get; private set; }
|
||||
|
||||
private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.ReleaseUnmanagedResources();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes window messages.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments.</param>
|
||||
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<byte>(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<DalamudInterface>.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);
|
||||
|
||||
/// <summary>
|
||||
/// Ported from imstb_textedit.h.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0xE2C)]
|
||||
private struct StbTextEditState
|
||||
{
|
||||
/// <summary>
|
||||
/// Position of the text cursor within the string.
|
||||
/// </summary>
|
||||
public int Cursor;
|
||||
|
||||
/// <summary>
|
||||
/// Selection start point.
|
||||
/// </summary>
|
||||
public int SelectStart;
|
||||
|
||||
/// <summary>
|
||||
/// selection start and end point in characters; if equal, no selection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.)
|
||||
/// </remarks>
|
||||
public int SelectEnd;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public byte InsertMode;
|
||||
|
||||
/// <summary>
|
||||
/// Page size in number of row.
|
||||
/// This value MUST be set to >0 for pageup or pagedown in multilines documents.
|
||||
/// </summary>
|
||||
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<char> TextWRaw;
|
||||
public ImVector<byte> TextARaw;
|
||||
public ImVector<byte> 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<char> TextW => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw));
|
||||
|
||||
public ImVectorWrapper<byte> TextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw));
|
||||
|
||||
public ImVectorWrapper<byte> 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<char> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
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 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the <see cref="ImeWindow"/>.
|
||||
/// Opens the <see cref="DalamudImeWindow"/>.
|
||||
/// </summary>
|
||||
public void OpenImeWindow() => this.imeWindow.IsOpen = true;
|
||||
|
||||
|
|
@ -356,7 +356,7 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
#region Close
|
||||
|
||||
/// <summary>
|
||||
/// Closes the <see cref="ImeWindow"/>.
|
||||
/// Closes the <see cref="DalamudImeWindow"/>.
|
||||
/// </summary>
|
||||
public void CloseImeWindow() => this.imeWindow.IsOpen = false;
|
||||
|
||||
|
|
@ -408,7 +408,7 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the <see cref="ImeWindow"/>.
|
||||
/// Toggles the <see cref="DalamudImeWindow"/>.
|
||||
/// </summary>
|
||||
public void ToggleImeWindow() => this.imeWindow.Toggle();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ using Dalamud.Configuration.Internal;
|
|||
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;
|
||||
|
|
@ -74,11 +73,15 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudIme dalamudIme = Service<DalamudIme>.Get();
|
||||
|
||||
private readonly ManualResetEvent fontBuildSignal;
|
||||
private readonly SwapChainVtableResolver address;
|
||||
private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
|
||||
private readonly Hook<SetCursorDelegate> setCursorHook;
|
||||
private Hook<ProcessMessageDelegate> processMessageHook;
|
||||
private RawDX11Scene? scene;
|
||||
|
||||
private Hook<PresentDelegate>? presentHook;
|
||||
|
|
@ -92,8 +95,6 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
[ServiceManager.ServiceConstructor]
|
||||
private InterfaceManager()
|
||||
{
|
||||
this.dispatchMessageWHook = Hook<DispatchMessageWDelegate>.FromImport(
|
||||
null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour);
|
||||
this.setCursorHook = Hook<SetCursorDelegate>.FromImport(
|
||||
null, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
|
||||
|
||||
|
|
@ -111,12 +112,6 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
[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);
|
||||
|
||||
/// <summary>
|
||||
/// This event gets called each frame to facilitate ImGui drawing.
|
||||
/// </summary>
|
||||
|
|
@ -236,10 +231,9 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
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 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
|
||||
this.scene = newScene;
|
||||
Service<InterfaceManagerWithScene>.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 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
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<ProcessMessageDelegate>.FromAddress(wndProcAddress, this.ProcessMessageDetour);
|
||||
|
||||
this.setCursorHook.Enable();
|
||||
this.presentHook.Enable();
|
||||
this.resizeBuffersHook.Enable();
|
||||
this.dispatchMessageWHook.Enable();
|
||||
this.processMessageHook.Enable();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1124,25 +1126,6 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
this.isRebuildingFonts = false;
|
||||
}
|
||||
|
||||
private unsafe IntPtr ProcessMessageDetour(IntPtr hWnd, uint msg, ulong wParam, ulong lParam, IntPtr handeled)
|
||||
{
|
||||
var ime = Service<DalamudIME>.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
|
||||
|
|
|
|||
223
Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs
Normal file
223
Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Windowing;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// A window for displaying IME details.
|
||||
/// </summary>
|
||||
internal unsafe class DalamudImeWindow : Window
|
||||
{
|
||||
private const int ImePageSize = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DalamudImeWindow"/> class.
|
||||
/// </summary>
|
||||
public DalamudImeWindow()
|
||||
: base(
|
||||
"Dalamud IME",
|
||||
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoBackground)
|
||||
{
|
||||
this.Size = default(Vector2);
|
||||
|
||||
this.RespectCloseHotkey = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Draw()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void PostDraw()
|
||||
{
|
||||
if (Service<DalamudIme>.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A window for displaying IME details.
|
||||
/// </summary>
|
||||
internal unsafe class ImeWindow : Window
|
||||
{
|
||||
private const int ImePageSize = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImeWindow"/> class.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Draw()
|
||||
{
|
||||
if (this.IsOpen && Service<KeyState>.Get()[VirtualKey.SHIFT]) Service<DalamudInterface>.Get().CloseImeWindow();
|
||||
var ime = Service<DalamudIME>.GetNullable();
|
||||
|
||||
if (ime == null || !ime.IsEnabled)
|
||||
{
|
||||
ImGui.Text("IME is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ImGui.Text($"{ime.GetCursorPos()}");
|
||||
// ImGui.Text($"{ImGui.GetWindowViewport().WorkSize}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void PostDraw()
|
||||
{
|
||||
if (this.IsOpen && Service<KeyState>.Get()[VirtualKey.SHIFT]) Service<DalamudInterface>.Get().CloseImeWindow();
|
||||
var ime = Service<DalamudIME>.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);
|
||||
}
|
||||
}
|
||||
273
Dalamud/Interface/Internal/WndProcHookManager.cs
Normal file
273
Dalamud/Interface/Internal/WndProcHookManager.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A manifestation of "I can't believe this is required".
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class WndProcHookManager : IServiceType, IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new("WPHM");
|
||||
|
||||
private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
|
||||
private readonly Dictionary<HWND, nint> 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<DispatchMessageWDelegate>.FromImport(
|
||||
null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour);
|
||||
this.dispatchMessageWHook.Enable();
|
||||
fixed (void* pMessageName = $"{nameof(WndProcHookManager)}.{nameof(this.unhookSelfMessage)}")
|
||||
this.unhookSelfMessage = RegisterWindowMessageW((ushort*)pMessageName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="WndProcHookManager"/> class.
|
||||
/// </summary>
|
||||
~WndProcHookManager() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for overriding WndProc.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments.</param>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Called before WndProc.
|
||||
/// </summary>
|
||||
public event WndProcOverrideDelegate? PreWndProc;
|
||||
|
||||
/// <summary>
|
||||
/// Called after WndProc.
|
||||
/// </summary>
|
||||
public event WndProcOverrideDelegate? PostWndProc;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.disposed = true;
|
||||
this.dispatchMessageWHook.Dispose();
|
||||
this.ReleaseUnmanagedResources();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detour for <see cref="DispatchMessageW"/>. Used to discover new windows to hook.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message.</param>
|
||||
/// <returns>The original return value.</returns>
|
||||
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<HWND, uint, WPARAM, LPARAM, LRESULT>)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<HWND, uint, WPARAM, LPARAM, LRESULT>)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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for <see cref="WndProcOverrideDelegate"/>.
|
||||
/// </summary>
|
||||
public ref struct WndProcOverrideEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The handle of the target window of the message.
|
||||
/// </summary>
|
||||
public readonly HWND Hwnd;
|
||||
|
||||
/// <summary>
|
||||
/// The message.
|
||||
/// </summary>
|
||||
public ref uint Message;
|
||||
|
||||
/// <summary>
|
||||
/// The WPARAM.
|
||||
/// </summary>
|
||||
public ref WPARAM WParam;
|
||||
|
||||
/// <summary>
|
||||
/// The LPARAM.
|
||||
/// </summary>
|
||||
public ref LPARAM LParam;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WndProcOverrideEventArgs"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="hwnd">The handle of the target window of the message.</param>
|
||||
/// <param name="msg">The message.</param>
|
||||
/// <param name="wParam">The WPARAM.</param>
|
||||
/// <param name="lParam">The LPARAM.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.<br />
|
||||
/// Does nothing if changed from <see cref="WndProcHookManager.PostWndProc"/>.
|
||||
/// </summary>
|
||||
public bool SuppressCall { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the return value.<br />
|
||||
/// Has the return value from next window procedure, if accessed from <see cref="WndProcHookManager.PostWndProc"/>.
|
||||
/// </summary>
|
||||
public LRESULT ReturnValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ImGui viewport ID.
|
||||
/// </summary>
|
||||
public int ViewportId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this message is for the game window (the first viewport).
|
||||
/// </summary>
|
||||
public bool IsGameWindow => this.ViewportId == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="SuppressCall"/> to <c>true</c> and sets <see cref="ReturnValue"/>.
|
||||
/// </summary>
|
||||
/// <param name="returnValue">The new return value.</param>
|
||||
public void SuppressAndReturn(LRESULT returnValue)
|
||||
{
|
||||
this.ReturnValue = returnValue;
|
||||
this.SuppressCall = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="SuppressCall"/> to <c>true</c> and calls <see cref="DefWindowProcW"/>.
|
||||
/// </summary>
|
||||
public void SuppressWithDefault()
|
||||
{
|
||||
this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam);
|
||||
this.SuppressCall = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -427,6 +427,26 @@ public static class ImGuiHelpers
|
|||
/// <returns>Whether it is empty.</returns>
|
||||
public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the corresponding ImGui viewport ID for the given window handle.
|
||||
/// </summary>
|
||||
/// <param name="hwnd">The window handle.</param>
|
||||
/// <returns>The viewport ID, or -1 if not found.</returns>
|
||||
internal static unsafe int FindViewportId(nint hwnd)
|
||||
{
|
||||
if (!IsImGuiInitialized)
|
||||
return -1;
|
||||
|
||||
var viewports = new ImVectorWrapper<ImGuiViewportPtr>(&ImGui.GetPlatformIO().NativePtr->Viewports);
|
||||
for (var i = 0; i < viewports.LengthUnsafe; i++)
|
||||
{
|
||||
if (viewports.DataUnsafe[i].PlatformHandle == hwnd)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get data needed for each new frame.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue