Merge pull request #1564 from Soreepeong/fix/ime

This commit is contained in:
goat 2023-12-17 15:11:47 +01:00 committed by GitHub
commit b16ba879b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1310 additions and 472 deletions

View file

@ -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

View file

@ -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();
}
}

View file

@ -611,29 +611,51 @@ public enum SeIconChar
QuestRepeatable = 0xE0BF,
/// <summary>
/// The IME hiragana icon unicode character.
/// The [あ] character indicating that the Japanese IME is in full-width Hiragana input mode.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
ImeHiragana = 0xE020,
/// <summary>
/// The IME katakana icon unicode character.
/// The [ア] character indicating that the Japanese IME is in full-width Katakana input mode.
/// </summary>
ImeKatakana = 0xE021,
/// <summary>
/// The IME alphanumeric icon unicode character.
/// The [] character indicating that Japanese or Korean IME is in full-width Latin character input mode.
/// </summary>
ImeAlphanumeric = 0xE022,
/// <summary>
/// The IME katakana half-width icon unicode character.
/// The [_ア] character indicating that the Japanese IME is in half-width Katakana input mode.
/// </summary>
ImeKatakanaHalfWidth = 0xE023,
/// <summary>
/// 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.
/// </summary>
ImeAlphanumericHalfWidth = 0xE024,
/// <summary>
/// The [가] character indicating that the Korean IME is in Hangul input mode.
/// </summary>
/// <remarks>
/// Use <see cref="ImeAlphanumeric"/> and <see cref="ImeAlphanumericHalfWidth"/> for alphanumeric input mode,
/// toggled via Alt+=.
/// </remarks>
ImeKoreanHangul = 0xE025,
/// <summary>
/// The [中] character indicating that the Chinese IME is in Han character input mode.
/// </summary>
ImeChineseHan = 0xE026,
/// <summary>
/// The [英] character indicating that the Chinese IME is in Latin character input mode.
/// </summary>
ImeChineseLatin = 0xE027,
/// <summary>
/// The instance (1) icon unicode character.

View file

@ -0,0 +1,144 @@
using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Hooking.WndProcHook;
/// <summary>
/// Event arguments for <see cref="WndProcEventDelegate"/>,
/// and the manager for individual WndProc hook.
/// </summary>
internal sealed unsafe class WndProcEventArgs
{
private readonly WndProcHookManager owner;
private readonly delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT> oldWndProcW;
private readonly WndProcDelegate myWndProc;
private GCHandle gcHandle;
private bool released;
/// <summary>
/// Initializes a new instance of the <see cref="WndProcEventArgs"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="hwnd">The handle of the target window of the message.</param>
/// <param name="viewportId">The viewport ID.</param>
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<HWND, uint, WPARAM, LPARAM, LRESULT>)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);
/// <summary>
/// Gets the handle of the target window of the message.
/// </summary>
public HWND Hwnd { get; }
/// <summary>
/// Gets the ImGui viewport ID.
/// </summary>
public int ViewportId { get; }
/// <summary>
/// Gets or sets the message.
/// </summary>
public uint Message { get; set; }
/// <summary>
/// Gets or sets the WPARAM.
/// </summary>
public WPARAM WParam { get; set; }
/// <summary>
/// Gets or sets the LPARAM.
/// </summary>
public LPARAM LParam { get; set; }
/// <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>
/// Sets <see cref="SuppressCall"/> to <c>true</c> and sets <see cref="ReturnValue"/>.
/// </summary>
/// <param name="returnValue">The new return value.</param>
public void SuppressWithValue(LRESULT returnValue)
{
this.ReturnValue = returnValue;
this.SuppressCall = true;
}
/// <summary>
/// Sets <see cref="SuppressCall"/> to <c>true</c> and sets <see cref="ReturnValue"/> from the result of
/// <see cref="DefWindowProcW"/>.
/// </summary>
public void SuppressWithDefault()
{
this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam);
this.SuppressCall = true;
}
/// <inheritdoc cref="IDisposable.Dispose"/>
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;
}
}

View file

@ -0,0 +1,7 @@
namespace Dalamud.Hooking.WndProcHook;
/// <summary>
/// Delegate for overriding WndProc.
/// </summary>
/// <param name="args">The arguments.</param>
internal delegate void WndProcEventDelegate(WndProcEventArgs args);

View file

@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Logging.Internal;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Hooking.WndProcHook;
/// <summary>
/// Manages WndProc hooks for game main window and extra ImGui viewport windows.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class WndProcHookManager : IServiceType, IDisposable
{
private static readonly ModuleLog Log = new(nameof(WndProcHookManager));
private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
private readonly Dictionary<HWND, WndProcEventArgs> wndProcOverrides = new();
private HWND mainWindowHwnd;
[ServiceManager.ServiceConstructor]
private unsafe WndProcHookManager()
{
this.dispatchMessageWHook = Hook<DispatchMessageWDelegate>.FromImport(
null,
"user32.dll",
"DispatchMessageW",
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<InterfaceManager.InterfaceManagerWithScene>
.GetAsync()
.ContinueWith(r => this.mainWindowHwnd = (HWND)r.Result.Manager.GameWindowHandle);
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private unsafe delegate nint DispatchMessageWDelegate(MSG* msg);
/// <summary>
/// Called before WndProc.
/// </summary>
public event WndProcEventDelegate? PreWndProc;
/// <summary>
/// Called after WndProc.
/// </summary>
public event WndProcEventDelegate? PostWndProc;
/// <inheritdoc/>
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();
}
/// <summary>
/// Invokes <see cref="PreWndProc"/>.
/// </summary>
/// <param name="args">The arguments.</param>
internal void InvokePreWndProc(WndProcEventArgs args)
{
try
{
this.PreWndProc?.Invoke(args);
}
catch (Exception e)
{
Log.Error(e, $"{nameof(this.PreWndProc)} error");
}
}
/// <summary>
/// Invokes <see cref="PostWndProc"/>.
/// </summary>
/// <param name="args">The arguments.</param>
internal void InvokePostWndProc(WndProcEventArgs args)
{
try
{
this.PostWndProc?.Invoke(args);
}
catch (Exception e)
{
Log.Error(e, $"{nameof(this.PostWndProc)} error");
}
}
/// <summary>
/// Removes <paramref name="args"/> from the list of known WndProc overrides.
/// </summary>
/// <param name="args">Object to remove.</param>
internal void OnHookedWindowRemoved(WndProcEventArgs args)
{
if (!this.dispatchMessageWHook.IsDisposed)
this.wndProcOverrides.Remove(args.Hwnd);
}
/// <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(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);
}
}

View file

@ -0,0 +1,630 @@
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 System.Text.Unicode;
using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
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 CJK IME.
/// </summary>
[ServiceManager.EarlyLoadedService]
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]
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 Han(Chinese) input has been detected.
/// </summary>
public bool EncounteredHan { get; private set; }
/// <summary>
/// Gets a value indicating whether Hangul(Korean) input has been detected.
/// </summary>
public bool EncounteredHangul { get; private set; }
/// <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 (!ImGuiHelpers.IsImGuiInitialized)
return true;
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 char InputModeIcon { get; private set; }
private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588);
/// <inheritdoc/>
public void Dispose()
{
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
/// <summary>
/// Looks for the characters inside <paramref name="str"/> and enables fonts accordingly.
/// </summary>
/// <param name="str">The string.</param>
public void ReflectCharacterEncounters(string str)
{
foreach (var chr in str)
{
if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
{
if (Service<GameFontManager>.Get()
.GetFdtReader(GameFontFamilyAndSize.Axis12)
?.FindGlyph(chr) is null)
{
if (!this.EncounteredHan)
{
this.EncounteredHan = true;
Service<InterfaceManager>.Get().RebuildFonts();
}
}
}
if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
{
if (!this.EncounteredHangul)
{
this.EncounteredHangul = true;
Service<InterfaceManager>.Get().RebuildFonts();
}
}
}
}
/// <summary>
/// Processes window messages.
/// </summary>
/// <param name="args">The arguments.</param>
public void ProcessImeMessage(WndProcEventArgs 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.SuppressWithValue(0);
break;
case WM.WM_IME_STARTCOMPOSITION:
args.SuppressWithValue(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.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.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.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.SuppressWithValue(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;
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)
{
this.ClearState(hImc);
args.WParam = VK.VK_PROCESSKEY;
}
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);
}
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()
{
if (ImGuiHelpers.IsImGuiInitialized)
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 = (char)SeIconChar.ImeKoreanHangul;
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:
if (native)
this.InputModeIcon = (char)SeIconChar.ImeChineseHan;
else
this.InputModeIcon = (char)SeIconChar.ImeChineseLatin;
break;
default:
this.InputModeIcon = default;
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);
this.ReflectCharacterEncounters(newString);
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.ClearState(hImc);
return;
}
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(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;
textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd;
// Log.Information($"{nameof(this.ClearState)}");
}
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])));
this.ReflectCharacterEncounters(this.ImmCand[^1]);
}
}
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;
this.AssociatedViewport = data.WantVisible ? viewport : default;
}
[ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")]
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);
}
/// <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;
}
}
}

View file

@ -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();

View file

@ -6,15 +6,16 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
using System.Threading;
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.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Notifications;
@ -73,12 +74,16 @@ 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 +97,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 +114,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 +233,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 +656,17 @@ internal class InterfaceManager : IDisposable, IServiceType
this.scene = newScene;
Service<InterfaceManagerWithScene>.Provide(new(this));
this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
}
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.SuppressWithValue(r.Value);
this.dalamudIme.ProcessImeMessage(args);
}
/*
@ -778,10 +785,22 @@ internal class InterfaceManager : IDisposable, IServiceType
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(
@ -809,7 +828,8 @@ internal class InterfaceManager : IDisposable, IServiceType
this.loadedFontInfo[DefaultFont] = fontInfo;
}
if (fontPathKr != null && Service<DalamudConfiguration>.Get().EffectiveLanguage == "ko")
if (fontPathKr != null
&& (Service<DalamudConfiguration>.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul))
{
fontConfig.MergeMode = true;
fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean();
@ -818,6 +838,46 @@ internal class InterfaceManager : IDisposable, IServiceType
fontConfig.MergeMode = false;
}
if (fontPathCht != null && Service<DalamudConfiguration>.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<DalamudConfiguration>.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");
{
@ -1095,15 +1155,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 +1178,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

View file

@ -0,0 +1,266 @@
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 != 0;
var imeIconFont = InterfaceManager.DefaultFont;
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)
{
for (var dx = -2; dx <= 2; dx++)
{
for (var dy = -2; dy <= 2; dy++)
{
if (dx != 0 || dy != 0)
{
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
cursor + new Vector2(dx, dy),
ImGui.GetColorU32(ImGuiCol.WindowBg),
ime.InputModeIcon);
}
}
}
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
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)
{
for (var dx = -2; dx <= 2; dx++)
{
for (var dy = -2; dy <= 2; dy++)
{
if (dx != 0 || dy != 0)
{
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
cursor + new Vector2(dx, dy),
ImGui.GetColorU32(ImGuiCol.WindowBg),
ime.InputModeIcon);
}
}
}
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
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));
}
}
}
}

View file

@ -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);
}
}

View file

@ -426,6 +426,26 @@ public static class ImGuiHelpers
/// <param name="ptr">The pointer.</param>
/// <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.