diff --git a/ChatTwo/ChatTwo.csproj b/ChatTwo/ChatTwo.csproj index c459b02..a8a76a5 100755 --- a/ChatTwo/ChatTwo.csproj +++ b/ChatTwo/ChatTwo.csproj @@ -1,6 +1,6 @@ - 1.23.5 + 1.24.0 net8.0-windows enable enable diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index db45f12..51e0804 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -40,6 +40,9 @@ internal class Configuration : IPluginConfiguration public bool KeepInputFocus = true; public int MaxLinesToRender = 10_000; + public bool ShowEmotes = true; + public HashSet BlockedEmotes = []; + public bool FontsEnabled = true; public ExtraGlyphRanges ExtraGlyphRanges = 0; public float FontSize = 17f; @@ -88,6 +91,8 @@ internal class Configuration : IPluginConfiguration PlaySounds = other.PlaySounds; KeepInputFocus = other.KeepInputFocus; MaxLinesToRender = other.MaxLinesToRender; + ShowEmotes = other.ShowEmotes; + BlockedEmotes = other.BlockedEmotes; FontsEnabled = other.FontsEnabled; ExtraGlyphRanges = other.ExtraGlyphRanges; FontSize = other.FontSize; diff --git a/ChatTwo/EmoteCache.cs b/ChatTwo/EmoteCache.cs index ff3fb4b..b42bff3 100644 --- a/ChatTwo/EmoteCache.cs +++ b/ChatTwo/EmoteCache.cs @@ -39,7 +39,7 @@ public static class EmoteCache private static readonly Dictionary Cache = new(); - private static readonly string[] EmoteCodeArray = []; + public static readonly string[] EmoteCodeArray = []; static EmoteCache() { diff --git a/ChatTwo/Message.cs b/ChatTwo/Message.cs index 743e96d..8f144c1 100755 --- a/ChatTwo/Message.cs +++ b/ChatTwo/Message.cs @@ -72,7 +72,8 @@ internal class Message internal Dictionary Height { get; } = new(); internal Dictionary IsVisible { get; } = new(); - internal Message(ulong receiver, ulong contentId, ChatCode code, List sender, List content, SeString senderSource, SeString contentSource) { + internal Message(ulong receiver, ulong contentId, ChatCode code, List sender, List content, SeString senderSource, SeString contentSource) + { Receiver = receiver; ContentId = contentId; Date = DateTimeOffset.UtcNow; @@ -191,7 +192,7 @@ internal class Message var builder = new StringBuilder(); foreach (var word in text.Content.Split(" ")) { - if (EmoteCache.Exists(word)) + if (Plugin.Config.ShowEmotes && EmoteCache.Exists(word) && !Plugin.Config.BlockedEmotes.Contains(word)) { // We add all the previous collected text parts AddContentAfterURLCheck(builder.ToString(), text, chunk); diff --git a/ChatTwo/PayloadHandler.cs b/ChatTwo/PayloadHandler.cs index 0f956f9..e9a7946 100755 --- a/ChatTwo/PayloadHandler.cs +++ b/ChatTwo/PayloadHandler.cs @@ -207,7 +207,7 @@ public sealed class PayloadHandler { internal void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button) { - if (LogWindow.Plugin.Config.PlaySounds) + if (Plugin.Config.PlaySounds) UIModule.PlaySound(PopupSfx); switch (button) @@ -230,7 +230,7 @@ public sealed class PayloadHandler { DoHover(() => HoverStatus(status), hoverSize); break; case ItemPayload item: - if (LogWindow.Plugin.Config.NativeItemTooltips) + if (Plugin.Config.NativeItemTooltips) { if (!_handleTooltips || _hoveredItem != item.RawItemId) { @@ -302,7 +302,7 @@ public sealed class PayloadHandler { var x = isLeft ? window.X : LogWindow.LastWindowPos.X - atkSize.X; var y = Math.Clamp(window.Y - atkSize.Y, 0, float.MaxValue); - y -= isTop ? 0 : LogWindow.Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom + y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom atk->SetPosition((short) x, (short) y); } diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 522fe27..f4cfd9b 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -40,6 +40,8 @@ public sealed class Plugin : IDalamudPlugin [PluginService] internal static INotificationManager Notification { get; private set; } = null!; [PluginService] internal static IAddonLifecycle AddonLifecycle { get; private set; } = null!; + internal static Configuration Config = null!; + public readonly WindowSystem WindowSystem = new(PluginName); public SettingsWindow SettingsWindow { get; } public ChatLogWindow ChatLogWindow { get; } @@ -48,7 +50,6 @@ public sealed class Plugin : IDalamudPlugin public DebuggerWindow DebuggerWindow { get; } internal LegacyMessageImporterWindow LegacyMessageImporterWindow { get; } - internal Configuration Config { get; } internal Commands Commands { get; } internal XivCommonBase Common { get; } internal TextureCache TextureCache { get; } diff --git a/ChatTwo/Resources/Language.Designer.cs b/ChatTwo/Resources/Language.Designer.cs index 70e050e..d81ed9e 100755 --- a/ChatTwo/Resources/Language.Designer.cs +++ b/ChatTwo/Resources/Language.Designer.cs @@ -1868,6 +1868,33 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Blocked emotes. + /// + internal static string Options_Emote_BlockedEmotes { + get { + return ResourceManager.GetString("Options_Emote_BlockedEmotes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Emote. + /// + internal static string Options_Emote_EmoteTable { + get { + return ResourceManager.GetString("Options_Emote_EmoteTable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Emotes. + /// + internal static string Options_Emote_Tab { + get { + return ResourceManager.GetString("Options_Emote_Tab", resourceCulture); + } + } + /// /// Looks up a localized string similar to Extra glyphs can be added to {0}'s global font by enabling the checkboxes below. This will likely require increasing Dalamud's font atlas size.. /// @@ -2309,6 +2336,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Replaces words with their emote version, currently supports BetterTTV. + /// + internal static string Options_ShowEmotes_Desc { + get { + return ResourceManager.GetString("Options_ShowEmotes_Desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show emotes. + /// + internal static string Options_ShowEmotes_Name { + get { + return ResourceManager.GetString("Options_ShowEmotes_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Show the Novice Network join button next to the settings button if logged in as a mentor.. /// diff --git a/ChatTwo/Resources/Language.resx b/ChatTwo/Resources/Language.resx index 1a23366..f9547bf 100644 --- a/ChatTwo/Resources/Language.resx +++ b/ChatTwo/Resources/Language.resx @@ -1018,4 +1018,19 @@ Keeps the input focus, even if you enter battle or do other actions + + Show emotes + + + Replaces words with their emote version, currently supports BetterTTV + + + Emotes + + + Blocked emotes + + + Emote + diff --git a/ChatTwo/Ui/CommandHelpWindow.cs b/ChatTwo/Ui/CommandHelpWindow.cs index 165700c..b7d118c 100644 --- a/ChatTwo/Ui/CommandHelpWindow.cs +++ b/ChatTwo/Ui/CommandHelpWindow.cs @@ -31,7 +31,7 @@ public class CommandHelpWindow : Window { var width = 350; var scaledWidth = width * ImGuiHelpers.GlobalScale; var pos = LogWindow.LastWindowPos; - switch (LogWindow.Plugin.Config.CommandHelpSide) { + switch (Plugin.Config.CommandHelpSide) { case CommandHelpSide.Right: pos.X += LogWindow.LastWindowSize.X; break; diff --git a/ChatTwo/Ui/Popout.cs b/ChatTwo/Ui/Popout.cs index 3684d83..18a9e18 100644 --- a/ChatTwo/Ui/Popout.cs +++ b/ChatTwo/Ui/Popout.cs @@ -39,15 +39,16 @@ internal class Popout : Window public override void PreDraw() { - if (ChatLogWindow.Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == ChatLogWindow.Plugin.Config.ChosenStyle)?.Push(); + if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) + StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push(); Flags = ImGuiWindowFlags.None; - if (!ChatLogWindow.Plugin.Config.ShowPopOutTitleBar) + if (!Plugin.Config.ShowPopOutTitleBar) Flags |= ImGuiWindowFlags.NoTitleBar; - if (!ChatLogWindow.PopOutDocked[Idx]) { - var alpha = Tab.IndependentOpacity ? Tab.Opacity : ChatLogWindow.Plugin.Config.WindowAlpha; + if (!ChatLogWindow.PopOutDocked[Idx]) + { + var alpha = Tab.IndependentOpacity ? Tab.Opacity : Plugin.Config.WindowAlpha; BgAlpha = alpha / 100f; } } @@ -56,7 +57,7 @@ internal class Popout : Window { using var id = ImRaii.PushId($"popout-{Tab.Identifier}"); - if (!ChatLogWindow.Plugin.Config.ShowPopOutTitleBar) + if (!Plugin.Config.ShowPopOutTitleBar) { ImGui.TextUnformatted(Tab.Name); ImGui.Separator(); @@ -70,8 +71,8 @@ internal class Popout : Window { ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked(); - if (ChatLogWindow.Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == ChatLogWindow.Plugin.Config.ChosenStyle)?.Pop(); + if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) + StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop(); } public override void OnClose() diff --git a/ChatTwo/Ui/Settings.cs b/ChatTwo/Ui/Settings.cs index 30678a8..596066e 100755 --- a/ChatTwo/Ui/Settings.cs +++ b/ChatTwo/Ui/Settings.cs @@ -35,6 +35,7 @@ public sealed class SettingsWindow : Window { new Display(Mutable), new ChatLog(Plugin, Mutable), + new Emote(Plugin, Mutable), new Ui.SettingsTabs.Fonts(Mutable), new ChatColours(Plugin, Mutable), new Tabs(Plugin, Mutable), diff --git a/ChatTwo/Ui/SettingsTabs/Display.cs b/ChatTwo/Ui/SettingsTabs/Display.cs index e01182b..1e42e57 100755 --- a/ChatTwo/Ui/SettingsTabs/Display.cs +++ b/ChatTwo/Ui/SettingsTabs/Display.cs @@ -49,5 +49,7 @@ internal sealed class Display : ISettingsTab ImGuiUtil.OptionCheckbox(ref Mutable.CollapseDuplicateMessages, Language.Options_CollapseDuplicateMessages_Name, Language.Options_CollapseDuplicateMessages_Description); ImGui.Spacing(); + + ImGui.PopTextWrapPos(); } } diff --git a/ChatTwo/Ui/SettingsTabs/Emote.cs b/ChatTwo/Ui/SettingsTabs/Emote.cs new file mode 100644 index 0000000..9a0a8d4 --- /dev/null +++ b/ChatTwo/Ui/SettingsTabs/Emote.cs @@ -0,0 +1,69 @@ +using System.Numerics; +using ChatTwo.Resources; +using ChatTwo.Util; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; + +namespace ChatTwo.Ui.SettingsTabs; + +internal sealed class Emote : ISettingsTab +{ + private readonly Plugin Plugin; + private Configuration Mutable { get; } + + public string Name => Language.Options_Emote_Tab + "###tabs-emote"; + + private static SearchSelector.SelectorPopupOptions WordPopupOptions = null!; + + internal Emote(Plugin plugin, Configuration mutable) + { + Plugin = plugin; + Mutable = mutable; + + WordPopupOptions = new SearchSelector.SelectorPopupOptions + { + FilteredSheet = EmoteCache.EmoteCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)) + }; + } + + public void Draw(bool changed) + { + ImGui.PushTextWrapPos(); + + ImGuiUtil.OptionCheckbox(ref Mutable.ShowEmotes, Language.Options_ShowEmotes_Name, Language.Options_ShowEmotes_Desc); + ImGui.Spacing(); + + ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes); + ImGui.Spacing(); + + var buttonWidth = ImGui.GetContentRegionAvail().X / 3; + using (Plugin.FontManager.FontAwesome.Push()) + ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0)); + + if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions)) + Mutable.BlockedEmotes.Add(newWord); + + using var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner); + if (table) + { + ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable); + ImGui.TableSetupColumn("##Del", 0, 0.07f); + + ImGui.TableHeadersRow(); + + var copiedList = Mutable.BlockedEmotes.ToArray(); + foreach (var word in copiedList) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(word); + + ImGui.TableNextColumn(); + if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl)) + Mutable.BlockedEmotes.Remove(word); + } + } + + ImGui.PopTextWrapPos(); + } +} diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs index 6c4c0d0..ee5b2d1 100755 --- a/ChatTwo/Util/ImGuiUtil.cs +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -3,6 +3,7 @@ using System.Text; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; +using Dalamud.Interface.Components; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -260,6 +261,15 @@ internal static class ImGuiUtil return r; } + public static bool Button(string id, FontAwesomeIcon icon, bool disabled) + { + var clicked = false; + using (ImRaii.Disabled(disabled)) + clicked = ImGuiComponents.IconButton(id, icon); + + return clicked; + } + internal static bool CtrlShiftButton(string label, string tooltip = "") { var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true }; diff --git a/ChatTwo/Util/SearchSelector.cs b/ChatTwo/Util/SearchSelector.cs new file mode 100644 index 0000000..a6aa6e0 --- /dev/null +++ b/ChatTwo/Util/SearchSelector.cs @@ -0,0 +1,161 @@ +using System.Numerics; +using Dalamud.Interface.Utility; +using ImGuiNET; +using System.Collections; + +namespace ChatTwo.Util; + +// Modified from: https://github.com/UnknownX7/Hypostasis/blob/master/ImGui/ExcelSheet.cs +public static class SearchSelector +{ + private static string[]? FilteredSearchSheet; + + private static string SheetSearchText = null!; + private static string PrevSearchId = null!; + private static Type PrevSearchType = null!; + + + public record SelectorOptions + { + public Func FormatRow { get; init; } = row => row.ToString(); + public Func? SearchPredicate { get; init; } = null; + public Func? DrawSelectable { get; init; } = null; + public IEnumerable FilteredSheet { get; init; } = []; + public Vector2? Size { get; init; } = null; + } + + public record SelectorPopupOptions: SelectorOptions + { + public ImGuiPopupFlags PopupFlags { get; init; } = ImGuiPopupFlags.None; + public bool CloseOnSelection { get; init; } = false; + public Func IsSelected { get; init; } = _ => false; + } + + private static void SearchInput(string id, IEnumerable filteredSheet, Func searchPredicate) + { + if (ImGui.IsWindowAppearing() && ImGui.IsWindowFocused() && !ImGui.IsAnyItemActive()) + { + if (id != PrevSearchId) + { + if (typeof(string) != PrevSearchType) + { + SheetSearchText = string.Empty; + PrevSearchType = typeof(string); + } + + FilteredSearchSheet = null; + PrevSearchId = id; + } + + ImGui.SetKeyboardFocusHere(0); + } + + if (ImGui.InputTextWithHint("##ExcelSheetSearch", "Search", ref SheetSearchText, 128, ImGuiInputTextFlags.AutoSelectAll)) + FilteredSearchSheet = null; + + FilteredSearchSheet ??= filteredSheet.Where(s => searchPredicate(s, SheetSearchText)).ToArray(); + } + + public static bool SelectorPopup(string id, out string selected, SelectorPopupOptions? options = null, bool close = false) + { + + options ??= new SelectorPopupOptions(); + var sheet = options.FilteredSheet; + selected = string.Empty; + + if (close) + return false; + + ImGui.SetNextWindowSize(options.Size ?? new Vector2(0, 250 * ImGuiHelpers.GlobalScale)); + if (!ImGui.BeginPopupContextItem(id, options.PopupFlags)) + return false; + + SearchInput(id, sheet, options.SearchPredicate ?? ((row, s) => options.FormatRow(row).Contains(s, StringComparison.CurrentCultureIgnoreCase))); + + ImGui.BeginChild("SearchList", Vector2.Zero, true); + + var ret = false; + var drawSelectable = options.DrawSelectable ?? ((row, selected) => ImGui.Selectable(options.FormatRow(row), selected)); + using (var clipper = new ListClipper(FilteredSearchSheet!.Length)) + { + foreach (var i in clipper.Rows) + { + var searched = FilteredSearchSheet[i]; + ImGui.PushID(id); + if (!drawSelectable(searched, options.IsSelected(searched))) continue; + selected = searched; + ret = true; + ImGui.PopID(); + } + } + + // ImGui issue #273849, children keep popups from closing automatically + if (ret && options.CloseOnSelection) + ImGui.CloseCurrentPopup(); + + ImGui.EndChild(); + ImGui.EndPopup(); + return ret; + } +} + +public unsafe class ListClipper : IEnumerable<(int, int)>, IDisposable +{ + private ImGuiListClipperPtr Clipper; + private readonly int CurrentRows; + private readonly int CurrentColumns; + private readonly bool TwoDimensional; + private readonly int ItemRemainder; + + public int FirstRow { get; private set; } = -1; + public int CurrentRow { get; private set; } + public int DisplayEnd => Clipper.DisplayEnd; + + public IEnumerable Rows + { + get + { + while (Clipper.Step()) // Supposedly this calls End() + { + if (Clipper.ItemsHeight > 0 && FirstRow < 0) + FirstRow = (int)(ImGui.GetScrollY() / Clipper.ItemsHeight); + + for (var i = Clipper.DisplayStart; i < Clipper.DisplayEnd; i++) + { + CurrentRow = i; + yield return TwoDimensional ? i : i * CurrentColumns; + } + } + } + } + + private IEnumerable Columns + { + get + { + var cols = (ItemRemainder == 0 || CurrentRows != DisplayEnd || CurrentRow != DisplayEnd - 1) ? CurrentColumns : ItemRemainder; + for (var j = 0; j < cols; j++) + yield return j; + } + } + + public ListClipper(int items, int cols = 1, bool twoD = false, float itemHeight = 0) + { + TwoDimensional = twoD; + CurrentColumns = cols; + CurrentRows = TwoDimensional ? items : (int)MathF.Ceiling((float)items / CurrentColumns); + ItemRemainder = !TwoDimensional ? items % CurrentColumns : 0; + Clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + Clipper.Begin(CurrentRows, itemHeight); + } + + public IEnumerator<(int, int)> GetEnumerator() => (from i in Rows from j in Columns select (i, j)).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Dispose() + { + Clipper.Destroy(); // This also calls End() but I'm calling it anyway just in case + GC.SuppressFinalize(this); + } +}