From 51db04a5f5a49f7325a200671d8b57c70c6f205c Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 19 May 2024 16:33:53 +0200 Subject: [PATCH] - Prevent null string from being parsed - cursor pos adjustment through preview --- ChatTwo/Ui/ChatLogWindow.cs | 46 +++++++++----- ChatTwo/Ui/InputPreview.cs | 122 +++++++++++++++++++++++++++++++++++- ChatTwo/Util/ChunkUtil.cs | 14 ++++- 3 files changed, 161 insertions(+), 21 deletions(-) diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index 9d3f80c..3c05f73 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -49,10 +49,11 @@ public sealed class ChatLogWindow : Window } } + internal bool KeepFocusedThroughPreview; internal bool Activate; private int ActivatePos = -1; internal string Chat = string.Empty; - private readonly IDalamudTextureWrap? FontIcon; + internal readonly IDalamudTextureWrap? FontIcon; private readonly List InputBacklog = []; private int InputBacklogIdx = -1; private int LastTab { get; set; } @@ -685,8 +686,11 @@ public sealed class ChatLogWindow : Window var push = inputColour != null; using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push)) { - if (Activate) + if (Activate || KeepFocusedThroughPreview) + { + KeepFocusedThroughPreview = false; ImGui.SetKeyboardFocusHere(); + } var chatCopy = Chat; @@ -1397,6 +1401,11 @@ public sealed class ChatLogWindow : Window var ptr = new ImGuiInputTextCallbackDataPtr(data); + // Set the cursor pos to the user selected + if (Plugin.InputPreview.SelectedCursorPos != -1) + ptr.CursorPos = Plugin.InputPreview.SelectedCursorPos; + Plugin.InputPreview.SelectedCursorPos = -1; + CursorPos = ptr.CursorPos; if (data->EventFlag == ImGuiInputTextFlags.CallbackCompletion) { @@ -1529,21 +1538,7 @@ public sealed class ChatLogWindow : Window { if (chunk is IconChunk icon && FontIcon != null) { - var bounds = IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out var entry); - if (!bounds) - return; - - var texSize = new Vector2(FontIcon.Width, FontIcon.Height); - - var sizeRatio = Plugin.Config.FontSize / entry.Height; - var size = new Vector2(entry.Width, entry.Height) * sizeRatio * ImGuiHelpers.GlobalScale; - - var uv0 = new Vector2(entry.Left, entry.Top + 170) * 2 / texSize; - var uv1 = new Vector2(entry.Left + entry.Width, entry.Top + entry.Height + 170) * 2 / texSize; - - ImGui.Image(FontIcon.ImGuiHandle, size, uv0, uv1); - ImGuiUtil.PostPayload(chunk, handler); - + DrawIcon(chunk, icon, handler); return; } @@ -1621,6 +1616,23 @@ public sealed class ChatLogWindow : Window (useCustomItalicFont ? Plugin.FontManager.ItalicFont! : Plugin.FontManager.AxisItalic).Pop(); } + internal void DrawIcon(Chunk chunk, IconChunk icon, PayloadHandler? handler) + { + if (!IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out var entry)) + return; + + var texSize = new Vector2(FontIcon!.Width, FontIcon.Height); + + var sizeRatio = Plugin.Config.FontSize / entry.Height; + var size = new Vector2(entry.Width, entry.Height) * sizeRatio * ImGuiHelpers.GlobalScale; + + var uv0 = new Vector2(entry.Left, entry.Top + 170) * 2 / texSize; + var uv1 = new Vector2(entry.Left + entry.Width, entry.Top + entry.Height + 170) * 2 / texSize; + + ImGui.Image(FontIcon.ImGuiHandle, size, uv0, uv1); + ImGuiUtil.PostPayload(chunk, handler); + } + private string HashPlayer(string playerName, uint worldId) { var hashCode = $"{Salt}{playerName}{worldId}".GetHashCode(); diff --git a/ChatTwo/Ui/InputPreview.cs b/ChatTwo/Ui/InputPreview.cs index 726eca5..d738a0a 100644 --- a/ChatTwo/Ui/InputPreview.cs +++ b/ChatTwo/Ui/InputPreview.cs @@ -1,10 +1,12 @@ using System.Numerics; +using System.Reflection; using System.Text; using ChatTwo.Code; using ChatTwo.Resources; using ChatTwo.Util; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using ImGuiNET; @@ -20,6 +22,10 @@ public class InputPreview : Window private int LastLength; private Message? PreviewMessage; + private bool NextChunkIsAutoTranslate; + private int CursorPosition; + public int SelectedCursorPos = -1; + internal InputPreview(ChatLogWindow logWindow) : base("##chat2-inputpreview") { LogWindow = logWindow; @@ -85,7 +91,7 @@ public class InputPreview : Window using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) { ImGui.TextUnformatted(Language.Options_Preview_Header); - LogWindow.DrawChunks(PreviewMessage.Content); + DrawChunksPreview(PreviewMessage.Content); } var after = ImGui.GetCursorPosY(); ImGui.SetCursorPos(pos); @@ -112,8 +118,120 @@ public class InputPreview : Window ImGui.TextUnformatted(Language.Options_Preview_Header); var handler = LogWindow.HandlerLender.Borrow(); - LogWindow.DrawChunks(PreviewMessage.Content, true, handler); + DrawChunksPreview(PreviewMessage.Content, handler, unique: 10000); handler.Draw(); } } + + private void DrawChunksPreview(IReadOnlyList chunks, PayloadHandler? handler = null, float lineWidth = 0f, int unique = 0) + { + CursorPosition = 0; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + for (var i = 0; i < chunks.Count; i++) + { + if (chunks[i] is TextChunk text && string.IsNullOrEmpty(text.Content)) + continue; + + DrawChunkPreview(chunks[i], handler, lineWidth, unique); + + if (i < chunks.Count - 1) + { + ImGui.SameLine(); + } + else if (chunks[i].Link is EmotePayload && Plugin.Config.ShowEmotes) + { + // Emote payloads seem to not automatically put newlines, which + // is an issue when modern mode is disabled. + ImGui.SameLine(); + // Use default ImGui behavior for newlines. + ImGui.TextUnformatted(""); + } + } + } + + private void DrawChunkPreview(Chunk chunk, PayloadHandler? handler = null, float lineWidth = 0f, int unique = 0) + { + if (chunk is IconChunk icon && LogWindow.FontIcon != null) + { + LogWindow.DrawIcon(chunk, icon, handler); + if (icon.Icon != BitmapFontIcon.AutoTranslateBegin) + return; + + try + { + NextChunkIsAutoTranslate = true; + var key = (uint)typeof(AutoTranslatePayload).GetField("key", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(chunk.Link)!; + var group = (uint)typeof(AutoTranslatePayload).GetField("group", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(chunk.Link)!; + CursorPosition += $"".Length; + } + catch + { + // Ignore + } + + return; + } + + if (chunk is not TextChunk text) + return; + + if (chunk.Link is EmotePayload emotePayload && Plugin.Config.ShowEmotes) + { + var emoteSize = ImGui.CalcTextSize("W"); + emoteSize = emoteSize with { Y = emoteSize.X } * 1.5f; + + // TextWrap doesn't work for emotes, so we have to wrap them manually + if (ImGui.GetContentRegionAvail().X < emoteSize.X) + ImGui.NewLine(); + + // We only draw a dummy if it is still loading, in the case it failed we draw the actual name + var image = EmoteCache.GetEmote(emotePayload.Code); + if (image is { Failed: false }) + { + if (image.IsLoaded) + image.Draw(emoteSize); + else + ImGui.Dummy(emoteSize); + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(emotePayload.Code); + + CursorPosition += emotePayload.Code.Length; + return; + } + } + + + if (text.Link != null || NextChunkIsAutoTranslate) + { + NextChunkIsAutoTranslate = false; + + if (text.Link is ItemPayload) + CursorPosition += "".Length; + else if (text.Link is MapLinkPayload) + CursorPosition += "".Length; + else if (text.Link is EmotePayload emote) + CursorPosition += emote.Code.Length; + + ImGuiUtil.WrapText(text.Content, chunk, handler, LogWindow.DefaultText, lineWidth); + return; + } + + foreach (var letter in text.Content) + { + var letterSize = ImGui.CalcTextSize(letter.ToString()); + if (ImGui.GetContentRegionAvail().X < letterSize.X) + ImGui.NewLine(); + + CursorPosition++; + if (ImGui.Selectable($"{letter}##{CursorPosition + unique}", false, ImGuiSelectableFlags.None, letterSize)) + { + SelectedCursorPos = CursorPosition; + LogWindow.KeepFocusedThroughPreview = true; + } + ImGui.SameLine(); + } + ImGui.NewLine(); + } } diff --git a/ChatTwo/Util/ChunkUtil.cs b/ChatTwo/Util/ChunkUtil.cs index 501ba5a..de50713 100755 --- a/ChatTwo/Util/ChunkUtil.cs +++ b/ChatTwo/Util/ChunkUtil.cs @@ -50,7 +50,7 @@ internal static class ChunkUtil glow.Pop(); break; case PayloadType.AutoTranslateText: - chunks.Add(new IconChunk(source, link, BitmapFontIcon.AutoTranslateBegin)); + chunks.Add(new IconChunk(source, payload, BitmapFontIcon.AutoTranslateBegin)); var autoText = ((AutoTranslatePayload) payload).Text; Append(autoText.Substring(2, autoText.Length - 4)); chunks.Add(new IconChunk(source, link, BitmapFontIcon.AutoTranslateEnd)); @@ -121,7 +121,17 @@ internal static class ChunkUtil break; default: if (payload is ITextProvider textProvider) - Append(textProvider.Text); + { + // We don't want to parse any null string + var str = textProvider.Text; + var nulIndex = str.IndexOf('\0'); + if (nulIndex > 0) + str = str[..nulIndex]; + if (string.IsNullOrEmpty(str)) + break; + + Append(str); + } break; } }