Add typing state IPC integration for enhanced Chat2 input handling
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ public class ContextMenuIntegration {
|
|||||||
this.Available.Subscribe(() => this.Register());
|
this.Available.Subscribe(() => this.Register());
|
||||||
// Register if Chat 2 is already loaded.
|
// Register if Chat 2 is already loaded.
|
||||||
this.Register();
|
this.Register();
|
||||||
|
|
||||||
// Listen for context menu events.
|
// Listen for context menu events.
|
||||||
this.Invoke.Subscribe(this.Integration);
|
this.Invoke.Subscribe(this.Integration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Register() {
|
private void Register() {
|
||||||
// Register and save the registration ID.
|
// Register and save the registration ID.
|
||||||
this._id = this.Register.InvokeFunc();
|
this._id = this.Register.InvokeFunc();
|
||||||
@@ -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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user