diff --git a/ChatTwo/Ipc/QuickSymbolsIpc.cs b/ChatTwo/Ipc/QuickSymbolsIpc.cs new file mode 100644 index 0000000..4010df4 --- /dev/null +++ b/ChatTwo/Ipc/QuickSymbolsIpc.cs @@ -0,0 +1,112 @@ +using ChatTwo.Ui.Handler; +using Dalamud.Plugin.Ipc; + +namespace ChatTwo; + +public sealed class QuickSymbolsIpc : IDisposable +{ + private const string Owner = "ChatTwo"; + private const string WatchInput = "QuickSymbols.WatchInput"; + private const string ClosePicker = "QuickSymbols.ClosePicker"; + private const string SymbolSelected = "QuickSymbols.SymbolSelected"; + + private ICallGateSubscriber? watch; + private ICallGateSubscriber? close; + private ICallGateSubscriber? selected; + private Action? onSelected; + + // This is for tracking the input that most recently advertised itself as active. + // QuickSymbols will send the selected symbol back ONLY through IPC; ChatTwo will still own the buffer and caret. + private InputHandler? input; + + public QuickSymbolsIpc(Plugin _) + { + Register(); + } + + public void Watch(InputHandler handler, bool state) + { + // A false state means that this handler should no longer receive symbols. + // This will preserve any other handler that became operational post this one. + if (!state) + { + if (ReferenceEquals(input, handler)) + input = null; + + return; + } + + input = handler; + + try + { + // The active input tells QuickSymbols that it is safe to react + // to the user QuickSymbols keybind but only for this owner and while the input is active. + watch?.InvokeFunc(Owner); + } + catch + { + // QuickSymbols is optional. Won't do anything unless called. + } + } + + private void Register() + { + try + { + // These subscribers will only be utilized if QuickSymbols is loaded + // and the IPC available. Otherwise, ChatTwo will carry on functioning as it normally does. + watch = Plugin.Interface.GetIpcSubscriber(WatchInput); + close = Plugin.Interface.GetIpcSubscriber(ClosePicker); + selected = Plugin.Interface.GetIpcSubscriber(SymbolSelected); + + // QuickSymbols doesnt modify ChatTwo input itself. It simply displays + // the chosen symbol in this location and then ChatTwo should handle its insertion via its own InputHandler. + onSelected = OnSelected; + selected.Subscribe(onSelected); + } + catch (Exception ex) + { + Plugin.Log.Debug(ex, "QuickSymbols IPC could not be registered."); + } + } + + private void Close() + { + try + { + // This will terminate any picker session that was initiated for ChatTwo. + // It should prevent outdated QuickSymbols target from remaining once ChatTwo has been unloaded. + close?.InvokeFunc(Owner); + } + catch + { + + } + } + + private void OnSelected(string owner, string symbol) + { + // Multiple plugins may pick up on the same occurrence, + // therefore the owner key is to ensure that each setup stays separate. + if (owner != Owner || string.IsNullOrEmpty(symbol)) + return; + + // This approach is to ensure that the text buffer, blinking cursor and the + // restoration of focus are all managed by ChatTwo itself. + // For this to work, it channels the input directly through ChatTwo internal mechanisms. + input?.InsertText(symbol); + } + + public void Dispose() + { + // Any active picker is closed before the IPC subscriber goes away. + Close(); + + if (selected != null && onSelected != null) + { + selected.Unsubscribe(onSelected); + onSelected = null; + } + } +} diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 785e4ef..0e4a982 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.Numerics; using ChatTwo.Http; -using ChatTwo.Ipc; using ChatTwo.Resources; using ChatTwo.Ui; using ChatTwo.Ui.ChatLog; @@ -62,6 +61,7 @@ public sealed class Plugin : IDalamudPlugin public IpcManager Ipc { get; } public ExtraChat ExtraChat { get; } public TypingIpc TypingIpc { get; } + public QuickSymbolsIpc QuickSymbols { get; } public FontManager FontManager { get; } public readonly ServerCore ServerCore; @@ -132,6 +132,7 @@ public Plugin() Functions = new GameFunctions.GameFunctions(this); Ipc = new IpcManager(); TypingIpc = new TypingIpc(this); + QuickSymbols = new QuickSymbolsIpc(this); ExtraChat = new ExtraChat(); FontManager = new FontManager(); @@ -216,6 +217,7 @@ public void Dispose() DebuggerWindow?.Dispose(); SeStringDebugger?.Dispose(); + QuickSymbols?.Dispose(); TypingIpc?.Dispose(); ExtraChat?.Dispose(); Ipc?.Dispose(); diff --git a/ChatTwo/Ui/Handler/InputHandler.cs b/ChatTwo/Ui/Handler/InputHandler.cs index a7f108e..28dd092 100644 --- a/ChatTwo/Ui/Handler/InputHandler.cs +++ b/ChatTwo/Ui/Handler/InputHandler.cs @@ -1,5 +1,6 @@ -using System.Numerics; +using System.Numerics; using System.Runtime.InteropServices; +using System.Text; using ChatTwo.Code; using ChatTwo.GameFunctions; using ChatTwo.GameFunctions.Types; @@ -31,6 +32,11 @@ public class InputHandler public string ChatInput = string.Empty; + private string? pendingText; + private int pendingPos = -1; + private int qsFocusFrames; + private int qsCaretFrames; + public bool FocusedPreview; public bool Activate; public bool InputFocused; @@ -57,6 +63,19 @@ public InputHandler(IChatWindow mainWindow, Plugin plugin, string id) AutoCompleteHandler = new AutoCompleteHandler(this); } + public void InsertText(string text) + { + if (string.IsNullOrEmpty(text)) + return; + + pendingText += text; + pendingPos = CursorPos; + Activate = true; + ActivatePos = CursorPos; + qsFocusFrames = 6; + qsCaretFrames = 90; + } + public void DrawInputArea(Tab activeTab, float inputWidth, ref bool tellSpecial) { var inputType = activeTab.CurrentChannel.UseTempChannel @@ -87,9 +106,12 @@ public void DrawInputArea(Tab activeTab, float inputWidth, ref bool tellSpecial) using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push)) { var isChatEnabled = activeTab is { InputDisabled: false }; - if (isChatEnabled && (Activate || FocusedPreview)) + if (isChatEnabled && (Activate || FocusedPreview || qsFocusFrames > 0)) { FocusedPreview = false; + if (qsFocusFrames > 0) + qsFocusFrames--; + ImGui.SetKeyboardFocusHere(); } @@ -100,9 +122,29 @@ public void DrawInputArea(Tab activeTab, float inputWidth, ref bool tellSpecial) ImGui.SetNextItemWidth(inputWidth); ImGui.InputTextWithHint("##chat2-input", isChatEnabled ? "": Language.ChatLog_DisabledInput, ref ChatInput, 500, flags, Callback); } - var inputActive = ImGui.IsItemActive(); + var inputMin = ImGui.GetItemRectMin(); + var inputMax = ImGui.GetItemRectMax(); + var inputActive = ImGui.IsItemActive() || ImGui.IsItemFocused(); InputFocused = isChatEnabled && inputActive; + if (qsCaretFrames > 0) + qsCaretFrames--; + + Plugin.QuickSymbols.Watch(this, isChatEnabled && (inputActive || qsFocusFrames > 0 || qsCaretFrames > 0)); + + if (!inputActive && qsCaretFrames > 0) + { + var caretAlpha = (ImGui.GetTime() % 1.0) < 0.5 ? 0.95f : 0.35f; + var textWidth = ImGui.CalcTextSize(ChatInput).X; + var caretX = MathF.Min(inputMax.X - 5f, inputMin.X + 8f + textWidth); + var caretY = inputMin.Y + 4f; + ImGui.GetWindowDrawList().AddLine( + new Vector2(caretX, caretY), + new Vector2(caretX, inputMax.Y - 4f), + ImGui.GetColorU32(new Vector4(1f, 1f, 1f, caretAlpha)), + 1f); + } + var tooltipDraw = Plugin.Config.PreviewPosition is PreviewPosition.Tooltip && Plugin.InputPreview.IsDrawable; if (tooltipDraw && ImGui.IsItemHovered()) { @@ -232,6 +274,19 @@ private unsafe int Callback(scoped ref ImGuiInputTextCallbackData data) ActivatePos = -1; } + if (pendingText != null) + { + var insert = pendingText; + var pos = pendingPos > -1 ? Math.Min(pendingPos, data.BufTextLen) : data.CursorPos; + + pendingText = null; + pendingPos = -1; + + data.InsertChars(pos, insert); + data.CursorPos = pos + Encoding.UTF8.GetByteCount(insert); + data.SelectionStart = data.SelectionEnd = data.CursorPos; + } + Plugin.CommandHelpWindow.IsOpen = false; var text = MemoryHelper.ReadString((nint) data.Buf, data.BufTextLen); if (text.StartsWith('/'))