Add typing state IPC integration for enhanced Chat2 input handling

This commit is contained in:
Keda
2025-11-15 00:17:39 +01:00
parent c54efe5420
commit a6a93ed241
4 changed files with 150 additions and 4 deletions
+65
View File
@@ -0,0 +1,65 @@
using ChatTwo.Code;
using Dalamud.Plugin.Ipc;
namespace ChatTwo.Ipc;
using ChatInputState = (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType);
internal sealed class TypingIpc : IDisposable
{
private Plugin Plugin { get; }
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
private ICallGateProvider<ChatInputState, object?> StateChangedGate { get; }
private ChatInputState LastState;
private bool HasState;
internal TypingIpc(Plugin plugin)
{
Plugin = plugin;
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>("ChatTwo.GetChatInputState");
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("ChatTwo.ChatInputStateChanged");
StateQueryGate.RegisterFunc(GetState);
}
private ChatInputState BuildState()
{
var log = Plugin.ChatLogWindow;
var chat = log.Chat ?? string.Empty;
var hasText = !string.IsNullOrWhiteSpace(chat);
var usedChannel = Plugin.CurrentTab?.CurrentChannel;
var inputChannel = usedChannel is not null
? (usedChannel.UseTempChannel ? usedChannel.TempChannel : usedChannel.Channel)
: InputChannel.Invalid;
var channelType = inputChannel.ToChatType();
return (InputVisible: !log.IsHidden,
InputFocused: log.InputFocused,
HasText: hasText,
IsTyping: log.InputFocused && hasText,
TextLength: chat.Length,
ChannelType: channelType);
}
private ChatInputState GetState()
=> BuildState();
internal void Update()
{
var state = BuildState();
if (HasState && state.Equals(LastState))
return;
HasState = true;
LastState = state;
StateChangedGate.SendMessage(state);
}
public void Dispose()
{
StateQueryGate.UnregisterFunc();
}
}
+12
View File
@@ -56,6 +56,7 @@ public sealed class Plugin : IDalamudPlugin
internal MessageManager MessageManager { get; } internal MessageManager MessageManager { get; }
internal IpcManager Ipc { get; } internal IpcManager Ipc { get; }
internal ExtraChat ExtraChat { get; } internal ExtraChat ExtraChat { get; }
internal TypingIpc TypingIpc { get; }
internal FontManager FontManager { get; } internal FontManager FontManager { get; }
internal ServerCore ServerCore { get; } internal ServerCore ServerCore { get; }
@@ -97,6 +98,7 @@ public sealed class Plugin : IDalamudPlugin
Commands = new Commands(this); Commands = new Commands(this);
Functions = new GameFunctions.GameFunctions(this); Functions = new GameFunctions.GameFunctions(this);
Ipc = new IpcManager(); Ipc = new IpcManager();
TypingIpc = new TypingIpc(this);
ExtraChat = new ExtraChat(this); ExtraChat = new ExtraChat(this);
FontManager = new FontManager(); FontManager = new FontManager();
@@ -181,6 +183,7 @@ public sealed class Plugin : IDalamudPlugin
DebuggerWindow?.Dispose(); DebuggerWindow?.Dispose();
SeStringDebugger?.Dispose(); SeStringDebugger?.Dispose();
TypingIpc?.Dispose();
ExtraChat?.Dispose(); ExtraChat?.Dispose();
Ipc?.Dispose(); Ipc?.Dispose();
MessageManager?.DisposeAsync().AsTask().Wait(); MessageManager?.DisposeAsync().AsTask().Wait();
@@ -193,8 +196,14 @@ public sealed class Plugin : IDalamudPlugin
private void Draw() private void Draw()
{ {
ChatLogWindow.BeginFrame();
if (Config.HideInLoadingScreens && Condition[ConditionFlag.BetweenAreas]) if (Config.HideInLoadingScreens && Condition[ConditionFlag.BetweenAreas])
{
ChatLogWindow.FinalizeFrame();
TypingIpc?.Update();
return; return;
}
ChatLogWindow.HideStateCheck(); ChatLogWindow.HideStateCheck();
@@ -205,6 +214,9 @@ public sealed class Plugin : IDalamudPlugin
{ {
WindowSystem.Draw(); WindowSystem.Draw();
} }
ChatLogWindow.FinalizeFrame();
TypingIpc?.Update();
} }
internal void SaveConfig() internal void SaveConfig()
+18 -2
View File
@@ -40,6 +40,7 @@ public sealed class ChatLogWindow : Window
internal bool FocusedPreview; internal bool FocusedPreview;
internal bool Activate; internal bool Activate;
internal bool InputFocused { get; private set; }
private int ActivatePos = -1; private int ActivatePos = -1;
internal string Chat = string.Empty; internal string Chat = string.Empty;
private readonly List<string> InputBacklog = []; private readonly List<string> InputBacklog = [];
@@ -72,6 +73,7 @@ public sealed class ChatLogWindow : Window
private const uint ChatOpenSfx = 35u; private const uint ChatOpenSfx = 35u;
private const uint ChatCloseSfx = 3u; private const uint ChatCloseSfx = 3u;
private bool PlayedClosingSound = true; private bool PlayedClosingSound = true;
private bool DrewThisFrame;
private long FrameTime; // set every frame private long FrameTime; // set every frame
internal long LastActivityTime = Environment.TickCount64; internal long LastActivityTime = Environment.TickCount64;
@@ -430,6 +432,17 @@ public sealed class ChatLogWindow : Window
IsHidden = false; IsHidden = false;
} }
internal void BeginFrame()
{
DrewThisFrame = false;
}
internal void FinalizeFrame()
{
if (!DrewThisFrame)
InputFocused = false;
}
public override unsafe void PreOpenCheck() public override unsafe void PreOpenCheck()
{ {
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoFocusOnAppearing; Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoFocusOnAppearing;
@@ -500,6 +513,7 @@ public sealed class ChatLogWindow : Window
public override void Draw() public override void Draw()
{ {
DrewThisFrame = true;
try try
{ {
DrawChatLog(); DrawChatLog();
@@ -618,6 +632,8 @@ public sealed class ChatLogWindow : Window
ImGui.SetNextItemWidth(inputWidth); ImGui.SetNextItemWidth(inputWidth);
ImGui.InputTextWithHint("##chat2-input", isChatEnabled ? "": Language.ChatLog_DisabledInput, ref Chat, 500, flags, Callback); ImGui.InputTextWithHint("##chat2-input", isChatEnabled ? "": Language.ChatLog_DisabledInput, ref Chat, 500, flags, Callback);
} }
var inputActive = ImGui.IsItemActive();
InputFocused = isChatEnabled && inputActive;
var tooltipDraw = Plugin.Config.PreviewPosition is PreviewPosition.Tooltip && Plugin.InputPreview.IsDrawable; var tooltipDraw = Plugin.Config.PreviewPosition is PreviewPosition.Tooltip && Plugin.InputPreview.IsDrawable;
if (tooltipDraw && ImGui.IsItemHovered()) if (tooltipDraw && ImGui.IsItemHovered())
@@ -655,14 +671,14 @@ public sealed class ChatLogWindow : Window
} }
// Process keybinds that have modifiers while the chat is focused. // Process keybinds that have modifiers while the chat is focused.
if (ImGui.IsItemActive()) if (inputActive)
{ {
Plugin.Functions.KeybindManager.HandleKeybinds(KeyboardSource.ImGui, true, true); Plugin.Functions.KeybindManager.HandleKeybinds(KeyboardSource.ImGui, true, true);
LastActivityTime = FrameTime; LastActivityTime = FrameTime;
} }
// Only trigger unfocused if we are currently not calling the auto complete // Only trigger unfocused if we are currently not calling the auto complete
if (!Activate && !ImGui.IsItemActive() && AutoCompleteInfo == null) if (!Activate && !inputActive && AutoCompleteInfo == null)
{ {
if (Plugin.Config.PlaySounds && !PlayedClosingSound) if (Plugin.Config.PlaySounds && !PlayedClosingSound)
{ {
+53
View File
@@ -73,4 +73,57 @@ public class ContextMenuIntegration {
} }
``` ```
# Typing State IPC
If you need to know whether the player is currently interacting with Chat 2's
input box, subscribe to the typing IPC.
- `ChatTwo.GetChatInputState`: call this function to retrieve the current state.
- `ChatTwo.ChatInputStateChanged`: subscribe to this event to receive updates
whenever the state changes (and once immediately after subscribing).
Both IPC endpoints use the same tuple payload:
```
(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)
```
- `InputVisible`: `true` when Chat 2 is not hidden by user/cutscene/battle
settings.
- `InputFocused`: `true` while the Chat 2 input box currently has keyboard focus.
- `HasText`: `true` when the input buffer contains more than whitespace.
- `IsTyping`: convenience flag (`InputFocused && HasText`).
- `TextLength`: length of the raw input buffer.
- `ChannelType`: the `ChatTwo.Code.ChatType` representing the channel/mode that
will be used if the buffer is submitted. This value comes from the current
tab's `UsedChannel` (`ChatTwo/Configuration.cs`) which the plugin keeps in
sync by hooking the in-game shell (`ChatTwo/GameFunctions/Chat.cs`) and by
resolving temporary overrides inside the chat UI
(`ChatTwo/Ui/ChatLogWindow.cs:597`). `InputChannel` values are converted into
the exported `ChatType` via `ChatTwo/Code/InputChannelExt.ToChatType`.
Example usage:
```cs
public sealed class TypingIntegration {
private ICallGateSubscriber<(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)> GetChatInputState { get; }
private ICallGateSubscriber<(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)> ChatInputStateChanged { get; }
public TypingIntegration(DalamudPluginInterface @interface) {
this.GetChatInputState = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("ChatTwo.GetChatInputState");
this.ChatInputStateChanged = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("ChatTwo.ChatInputStateChanged");
}
public void Enable() {
this.ChatInputStateChanged.Subscribe(OnChatInputStateChanged);
// Optionally poll the current state on enable.
var state = this.GetChatInputState.InvokeFunc();
PluginLog.Information($"Initial typing state: {state}");
}
public void Disable() {
this.ChatInputStateChanged.Unsubscribe(OnChatInputStateChanged);
}
private void OnChatInputStateChanged((bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType) state) {
if (state.IsTyping) {
// Show typing indicator.
} else {
// Hide typing indicator.
}
}
}
```
All integrations are called inside of an ImGui `BeginMenu`. All integrations are called inside of an ImGui `BeginMenu`.