diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index ace8887f1..b64df8f19 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -96,12 +96,6 @@ internal class DalamudCommands : IServiceType ShowInHelp = false, }); - commandManager.AddHandler("/xlime", new CommandInfo(this.OnDebugDrawIMEPanel) - { - HelpMessage = Loc.Localize("DalamudIMEPanelHelp", "Draw IME panel"), - ShowInHelp = false, - }); - commandManager.AddHandler("/xllog", new CommandInfo(this.OnOpenLog) { HelpMessage = Loc.Localize("DalamudDevLogHelp", "Open dev log DEBUG"), @@ -308,11 +302,6 @@ internal class DalamudCommands : IServiceType dalamudInterface.ToggleDataWindow(arguments); } - private void OnDebugDrawIMEPanel(string command, string arguments) - { - Service.Get().OpenImeWindow(); - } - private void OnOpenLog(string command, string arguments) { Service.Get().ToggleLogWindow(); diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 1ee248b17..6c01b74d7 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -17,6 +18,8 @@ using Dalamud.Interface.Utility; using ImGuiNET; +using Serilog; + using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; @@ -26,12 +29,21 @@ namespace Dalamud.Interface.Internal; /// /// This class handles CJK IME. /// -[ServiceManager.BlockingEarlyLoadedService] +[ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType { private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; private const int CImGuiStbTextUndoOffset = 0xB59C0; + private const int ImePageSize = 9; + + private static readonly Dictionary WmNames = + typeof(WM).GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(x => x.IsLiteral && !x.IsInitOnly && x.FieldType == typeof(int)) + .Select(x => ((int)x.GetRawConstantValue()!, x.Name)) + .DistinctBy(x => x.Item1) + .ToDictionary(x => x.Item1, x => x.Name); + private static readonly UnicodeRange[] HanRange = { UnicodeRanges.CjkRadicalsSupplement, @@ -57,8 +69,41 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private static readonly delegate* unmanaged StbTextUndo; + [ServiceManager.ServiceDependency] + private readonly WndProcHookManager wndProcHookManager = Service.Get(); + + private readonly InterfaceManager interfaceManager; + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; + /// The candidates. + private readonly List candidateStrings = new(); + + /// The selected imm component. + private string compositionString = string.Empty; + + /// The cursor position in screen coordinates. + private Vector2 cursorScreenPos; + + /// The associated viewport. + private ImGuiViewportPtr associatedViewport; + + /// The index of the first imm candidate in relation to the full list. + private CANDIDATELIST immCandNative; + + /// The partial conversion from-range. + private int partialConversionFrom; + + /// The partial conversion to-range. + private int partialConversionTo; + + /// The cursor offset in the composition string. + private int compositionCursorOffset; + + /// The input mode icon from . + private char inputModeIcon; + + /// Undo range for modifying the buffer while composition is in progress. private (int Start, int End, int Cursor)? temporaryUndoSelection; [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")] @@ -87,7 +132,17 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } [ServiceManager.ServiceConstructor] - private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; + private DalamudIme(InterfaceManager.InterfaceManagerWithScene imws) + { + Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); + + this.interfaceManager = imws.Manager; + this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; + + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + this.interfaceManager.Draw += this.Draw; + this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; + } /// /// Finalizes an instance of the class. @@ -109,7 +164,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. /// - internal static bool ShowCursorInInputText + private static bool ShowCursorInInputText { get { @@ -126,63 +181,21 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - /// - /// Gets the cursor position, in screen coordinates. - /// - internal Vector2 CursorPos { get; private set; } - - /// - /// Gets the associated viewport. - /// - internal ImGuiViewportPtr AssociatedViewport { get; private set; } - - /// - /// Gets the index of the first imm candidate in relation to the full list. - /// - internal CANDIDATELIST ImmCandNative { get; private set; } - - /// - /// Gets the imm candidates. - /// - internal List ImmCand { get; private set; } = new(); - - /// - /// Gets the selected imm component. - /// - internal string ImmComp { get; private set; } = string.Empty; - - /// - /// Gets the partial conversion from-range. - /// - internal int PartialConversionFrom { get; private set; } - - /// - /// Gets the partial conversion to-range. - /// - internal int PartialConversionTo { get; private set; } - - /// - /// Gets the cursor offset in the composition string. - /// - internal int CompositionCursorOffset { get; private set; } - - /// - /// Gets a value indicating whether to display partial conversion status. - /// - internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || - this.PartialConversionTo != this.ImmComp.Length; - - /// - /// Gets the input mode icon from . - /// - internal char InputModeIcon { get; private set; } - private static ImGuiInputTextState* TextState => (ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset); + /// Gets a value indicating whether to display partial conversion status. + private bool ShowPartialConversion => this.partialConversionFrom != 0 || + this.partialConversionTo != this.compositionString.Length; + + /// Gets a value indicating whether to draw. + private bool ShouldDraw => + this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default; + /// public void Dispose() { + this.interfaceManager.Draw -= this.Draw; this.ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } @@ -195,13 +208,13 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { foreach (var chr in str) { - if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + if (!this.EncounteredHan) { - if (Service.Get() - ?.GetFdtReader(GameFontFamilyAndSize.Axis12) - .FindGlyph(chr) is null) + if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (!this.EncounteredHan) + if (Service.Get() + ?.GetFdtReader(GameFontFamilyAndSize.Axis12) + .FindGlyph(chr) is null) { this.EncounteredHan = true; Service.Get().RebuildFonts(); @@ -209,9 +222,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + if (!this.EncounteredHangul) { - if (!this.EncounteredHangul) + if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { this.EncounteredHangul = true; Service.Get().RebuildFonts(); @@ -220,11 +233,24 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - /// - /// Processes window messages. - /// - /// The arguments. - public void ProcessImeMessage(WndProcEventArgs args) + 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 WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { if (!ImGuiHelpers.IsImGuiInitialized) return; @@ -246,7 +272,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: - this.UpdateImeWindowStatus(hImc); + this.UpdateCandidates(hImc); args.SuppressWithValue(0); break; @@ -260,22 +286,22 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType else this.ReplaceCompositionString(hImc, (uint)args.LParam); - // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.compositionString}"); 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}"); + // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); 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}"); + // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); 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}"); + // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithValue(0); break; @@ -283,12 +309,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // 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}"); + // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithDefault(); break; case WM.WM_IME_NOTIFY: - // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.compositionString}"); break; case WM.WM_KEYDOWN when (int)args.WParam is @@ -302,12 +328,14 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType or VK.VK_RIGHT or VK.VK_DOWN or VK.VK_RETURN: - if (this.ImmCand.Count != 0) + if (this.candidateStrings.Count != 0) { this.ClearState(hImc); args.WParam = VK.VK_PROCESSKEY; } + this.UpdateCandidates(hImc); + break; case WM.WM_LBUTTONDOWN: @@ -316,9 +344,15 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType case WM.WM_XBUTTONDOWN: ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); break; + + // default: + // Log.Verbose($"{(WmNames.TryGetValue((int)args.Message, out var v) ? v : args.Message.ToString())}({(nint)args.WParam:X}, {(nint)args.LParam:X})"); + // break; } this.UpdateInputLanguage(hImc); + if (this.inputModeIcon == (char)SeIconChar.ImeKoreanHangul) + this.UpdateCandidates(hImc); } finally { @@ -326,23 +360,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - 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; @@ -359,41 +376,39 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = (char)SeIconChar.ImeKoreanHangul; + this.inputModeIcon = (char)SeIconChar.ImeKoreanHangul; else if (fullwidth) - this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_JAPANESE: // wtf // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 if (open && native && katakana && fullwidth) - this.InputModeIcon = (char)SeIconChar.ImeKatakana; + this.inputModeIcon = (char)SeIconChar.ImeKatakana; else if (open && native && katakana) - this.InputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth; + this.inputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth; else if (open && native) - this.InputModeIcon = (char)SeIconChar.ImeHiragana; + this.inputModeIcon = (char)SeIconChar.ImeHiragana; else if (open && fullwidth) - this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_CHINESE: if (native) - this.InputModeIcon = (char)SeIconChar.ImeChineseHan; + this.inputModeIcon = (char)SeIconChar.ImeChineseHan; else - this.InputModeIcon = (char)SeIconChar.ImeChineseLatin; + this.inputModeIcon = (char)SeIconChar.ImeChineseLatin; break; default: - this.InputModeIcon = default; + this.inputModeIcon = default; break; } - - this.UpdateImeWindowStatus(hImc); } private void ReplaceCompositionString(HIMC hImc, uint comp) @@ -425,14 +440,14 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return; } - this.ImmComp = newString; - this.CompositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + this.compositionString = newString; + this.compositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); var attrPtr = stackalloc byte[attrLength]; - var attr = new Span(attrPtr, Math.Min(this.ImmComp.Length, attrLength)); + var attr = new Span(attrPtr, Math.Min(this.compositionString.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) @@ -442,37 +457,37 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType 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); + if (r == 0 || l == this.compositionString.Length) + (l, r) = (0, this.compositionString.Length); - (this.PartialConversionFrom, this.PartialConversionTo) = (l, r); + (this.partialConversionFrom, this.partialConversionTo) = (l, r); } else { - this.PartialConversionFrom = 0; - this.PartialConversionTo = this.ImmComp.Length; + this.partialConversionFrom = 0; + this.partialConversionTo = this.compositionString.Length; } - this.UpdateImeWindowStatus(hImc); + this.UpdateCandidates(hImc); } private void ClearState(HIMC hImc) { - this.ImmComp = string.Empty; - this.PartialConversionFrom = this.PartialConversionTo = 0; - this.CompositionCursorOffset = 0; + this.compositionString = string.Empty; + this.partialConversionFrom = this.partialConversionTo = 0; + this.compositionCursorOffset = 0; this.temporaryUndoSelection = null; TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd; ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); - this.UpdateImeWindowStatus(default); + this.UpdateCandidates(default); // Log.Information($"{nameof(this.ClearState)}"); } - private void LoadCand(HIMC hImc) + private void UpdateCandidates(HIMC hImc) { - this.ImmCand.Clear(); - this.ImmCandNative = default; + this.candidateStrings.Clear(); + this.immCandNative = default; if (hImc == default) return; @@ -486,7 +501,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return; ref var candlist = ref *(CANDIDATELIST*)pStorage; - this.ImmCandNative = candlist; + this.immCandNative = candlist; if (candlist.dwPageSize == 0 || candlist.dwCount == 0) return; @@ -495,39 +510,250 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType (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]); + this.candidateStrings.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + this.ReflectCharacterEncounters(this.candidateStrings[^1]); } } - private void UpdateImeWindowStatus(HIMC hImc) - { - if (Service.GetNullable() is not { } di) - return; - - this.LoadCand(hImc); - if (this.ImmCand.Count != 0 || this.ShowPartialConversion || this.InputModeIcon != default) - di.OpenImeWindow(); - else - di.CloseImeWindow(); - } - private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) { - this.CursorPos = data.InputPos; - this.AssociatedViewport = data.WantVisible ? viewport : default; + this.cursorScreenPos = data.InputPos; + this.associatedViewport = data.WantVisible ? viewport : default; } - [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui context initialization.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + private void Draw() { - if (!ImGuiHelpers.IsImGuiInitialized) + if (!this.ShouldDraw) + return; + + if (Service.GetNullable() is not { } ime) + return; + + var viewport = ime.associatedViewport; + if (viewport.NativePtr is null) + return; + + var drawCand = ime.candidateStrings.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.compositionString == string.Empty ? " " : ime.compositionString); + + 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.candidateStrings.Count; i++) { - throw new InvalidOperationException( - $"Expected {nameof(InterfaceManager.InterfaceManagerWithScene)} to have initialized ImGui."); + var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.candidateStrings[i]}"); + maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; } - ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.compositionString).X + ? maxTextWidth + : ImGui.CalcTextSize(ime.compositionString).X; + + var numEntries = (drawCand ? ime.candidateStrings.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.cursorScreenPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y; + var windowPos = ime.cursorScreenPos - 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.candidateStrings.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.candidateStrings[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.compositionString.Length) + { + var part1 = ime.compositionString[..ime.partialConversionFrom]; + var part2 = ime.compositionString[..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.compositionString); + + // Draw the caret inside the composition string. + if (DalamudIme.ShowCursorInInputText) + { + var partBeforeCaret = ime.compositionString[..ime.compositionCursorOffset]; + var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret); + drawList.AddLine( + cursor + sizeBeforeCaret with { Y = 0 }, + cursor + sizeBeforeCaret, + ImGui.GetColorU32(ImGuiCol.Text)); + } + } } /// diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 00bef19af..1a07cd6ae 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -61,7 +61,6 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly ComponentDemoWindow componentDemoWindow; private readonly DataWindow dataWindow; private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow; - private readonly DalamudImeWindow imeWindow; private readonly ConsoleWindow consoleWindow; private readonly PluginStatWindow pluginStatWindow; private readonly PluginInstallerWindow pluginWindow; @@ -114,7 +113,6 @@ 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 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 }; @@ -142,7 +140,6 @@ internal class DalamudInterface : IDisposable, IServiceType this.WindowSystem.AddWindow(this.componentDemoWindow); this.WindowSystem.AddWindow(this.dataWindow); this.WindowSystem.AddWindow(this.gamepadModeNotifierWindow); - this.WindowSystem.AddWindow(this.imeWindow); this.WindowSystem.AddWindow(this.consoleWindow); this.WindowSystem.AddWindow(this.pluginStatWindow); this.WindowSystem.AddWindow(this.pluginWindow); @@ -265,11 +262,6 @@ internal class DalamudInterface : IDisposable, IServiceType /// public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true; - /// - /// Opens the . - /// - public void OpenImeWindow() => this.imeWindow.IsOpen = true; - /// /// Opens the . /// @@ -365,11 +357,6 @@ internal class DalamudInterface : IDisposable, IServiceType #region Close - /// - /// Closes the . - /// - public void CloseImeWindow() => this.imeWindow.IsOpen = false; - /// /// Closes the . /// @@ -417,11 +404,6 @@ internal class DalamudInterface : IDisposable, IServiceType /// public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle(); - /// - /// Toggles the . - /// - public void ToggleImeWindow() => this.imeWindow.Toggle(); - /// /// Toggles the . /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3db799be0..126097ed3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -67,9 +67,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly DalamudIme dalamudIme = Service.Get(); private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; @@ -627,8 +624,6 @@ internal class InterfaceManager : IDisposable, IServiceType 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); } /* diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs deleted file mode 100644 index ecaa522e5..000000000 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System.Numerics; - -using Dalamud.Interface.Windowing; - -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Windows; - -/// -/// A window for displaying IME details. -/// -internal unsafe class DalamudImeWindow : Window -{ - private const int ImePageSize = 9; - - /// - /// Initializes a new instance of the class. - /// - public DalamudImeWindow() - : base( - "Dalamud IME", - ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoBackground) - { - this.Size = default(Vector2); - - this.RespectCloseHotkey = false; - } - - /// - public override void Draw() - { - } - - /// - public override void PostDraw() - { - if (Service.GetNullable() is not { } ime) - return; - - var viewport = ime.AssociatedViewport; - if (viewport.NativePtr is null) - return; - - var drawCand = ime.ImmCand.Count != 0; - var drawConv = drawCand || ime.ShowPartialConversion; - var drawIme = ime.InputModeIcon != 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)); - } - } - } -} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 3ff7cde76..acd7c2b6f 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -165,6 +165,7 @@ internal static class ServiceManager var earlyLoadingServices = new HashSet(); var blockingEarlyLoadingServices = new HashSet(); + var providedServices = new HashSet(); var dependencyServicesMap = new Dictionary>(); var getAsyncTaskMap = new Dictionary(); @@ -197,7 +198,10 @@ internal static class ServiceManager // We don't actually need to load provided services, something else does if (serviceKind.HasFlag(ServiceKind.ProvidedService)) + { + providedServices.Add(serviceType); continue; + } Debug.Assert( serviceKind.HasFlag(ServiceKind.EarlyLoadedService) || @@ -340,7 +344,16 @@ internal static class ServiceManager } if (!tasks.Any()) - throw new InvalidOperationException("Unresolvable dependency cycle detected"); + { + // No more services we can start loading for now. + // Either we're waiting for provided services, or there's a dependency cycle. + providedServices.RemoveWhere(x => getAsyncTaskMap[x].IsCompleted); + if (providedServices.Any()) + await Task.WhenAny(providedServices.Select(x => getAsyncTaskMap[x])); + else + throw new InvalidOperationException("Unresolvable dependency cycle detected"); + continue; + } if (servicesToLoad.Any()) {