From 352088dfed284f80cbe33ea4793eb753ffebdb49 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 11 Jul 2024 00:32:13 +1000 Subject: [PATCH 1/2] feat: add configurable hotkeys to cycle tabs Adds two configurable hotkeys (plus the required code infrastructure to handle configurable hotkeys) for cycling the active chat tab forward by one and backwards by one. --- ChatTwo/Configuration.cs | 26 ++++++++ ChatTwo/GameFunctions/Chat.cs | 45 ++++++++----- ChatTwo/GameFunctions/Types/ModifierFlag.cs | 1 + ChatTwo/Resources/Language.Designer.cs | 63 +++++++++++++++++++ ChatTwo/Resources/Language.resx | 21 +++++++ ChatTwo/Ui/ChatLogWindow.cs | 62 +++++++++++++----- ChatTwo/Ui/SettingsTabs/ChatLog.cs | 12 ++++ ChatTwo/Util/ImGuiUtil.cs | 70 +++++++++++++++++++++ 8 files changed, 272 insertions(+), 28 deletions(-) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 2afa0c7..139282a 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -1,12 +1,33 @@ using System.Collections; using ChatTwo.Code; +using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using ChatTwo.Ui; using Dalamud.Configuration; +using Dalamud.Game.ClientState.Keys; using ImGuiNET; namespace ChatTwo; +[Serializable] +internal class ConfigKeyBind +{ + public ModifierFlag Modifier; + public VirtualKey Key; + + public override string ToString() + { + var modString = ""; + if (Modifier.HasFlag(ModifierFlag.Ctrl)) + modString += Language.Keybind_Modifier_Ctrl + " + "; + if (Modifier.HasFlag(ModifierFlag.Shift)) + modString += Language.Keybind_Modifier_Shift + " + "; + if (Modifier.HasFlag(ModifierFlag.Alt)) + modString += Language.Keybind_Modifier_Alt + " + "; + return modString+Key.GetFancyName(); + } +} + [Serializable] internal class Configuration : IPluginConfiguration { @@ -68,6 +89,9 @@ internal class Configuration : IPluginConfiguration public bool OverrideStyle; public string? ChosenStyle; + public ConfigKeyBind? ChatTabForward = null; + public ConfigKeyBind? ChatTabBackward = null; + internal void UpdateFrom(Configuration other, bool backToOriginal) { if (backToOriginal) @@ -123,6 +147,8 @@ internal class Configuration : IPluginConfiguration Tabs = other.Tabs.Select(t => t.Clone()).ToList(); OverrideStyle = other.OverrideStyle; ChosenStyle = other.ChosenStyle; + ChatTabForward = other.ChatTabForward; + ChatTabBackward = other.ChatTabBackward; } } diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index 902fd99..d36b307 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -266,6 +266,36 @@ internal sealed unsafe class Chat : IDisposable modifierState |= modifier; } + bool KeyPressed(VirtualKey key, ModifierFlag modifier) + { + if (!Plugin.KeyState.IsVirtualKeyValid(key)) + return false; + + var modifierPressed = Plugin.Config.KeybindMode switch + { + KeybindMode.Strict => modifier == modifierState, + KeybindMode.Flexible => modifierState.HasFlag(modifier), + _ => false, + }; + + return modifierPressed && Plugin.KeyState[key]; + } + + // Test for custom keybinds for changing chat tabs before checking + // vanilla keybinds. + if (Plugin.Config.ChatTabBackward != null && KeyPressed(Plugin.Config.ChatTabBackward.Key, Plugin.Config.ChatTabBackward.Modifier)) + { + Plugin.KeyState[Plugin.Config.ChatTabBackward.Key] = false; + Plugin.ChatLogWindow.ChangeTabDelta(-1); + return; + } + if (Plugin.Config.ChatTabForward != null && KeyPressed(Plugin.Config.ChatTabForward.Key, Plugin.Config.ChatTabForward.Modifier)) + { + Plugin.KeyState[Plugin.Config.ChatTabForward.Key] = false; + Plugin.ChatLogWindow.ChangeTabDelta(1); + return; + } + var turnedOff = new Dictionary(); foreach (var toIntercept in KeybindsToIntercept.Keys) { @@ -281,20 +311,7 @@ internal sealed unsafe class Chat : IDisposable void Intercept(VirtualKey key, ModifierFlag modifier) { - if (!Plugin.KeyState.IsVirtualKeyValid(key)) - return; - - var modifierPressed = Plugin.Config.KeybindMode switch - { - KeybindMode.Strict => modifier == modifierState, - KeybindMode.Flexible => modifierState.HasFlag(modifier), - _ => false, - }; - - if (!modifierPressed) - return; - - if (!Plugin.KeyState[key]) + if (!KeyPressed(key, modifier)) return; var bits = BitOperations.PopCount((uint) modifier); diff --git a/ChatTwo/GameFunctions/Types/ModifierFlag.cs b/ChatTwo/GameFunctions/Types/ModifierFlag.cs index 4fc9382..395055e 100755 --- a/ChatTwo/GameFunctions/Types/ModifierFlag.cs +++ b/ChatTwo/GameFunctions/Types/ModifierFlag.cs @@ -3,6 +3,7 @@ namespace ChatTwo.GameFunctions.Types; [Flags] internal enum ModifierFlag { + None = 0, Shift = 1 << 0, Ctrl = 1 << 1, Alt = 1 << 2, diff --git a/ChatTwo/Resources/Language.Designer.cs b/ChatTwo/Resources/Language.Designer.cs index 606f01a..def1633 100755 --- a/ChatTwo/Resources/Language.Designer.cs +++ b/ChatTwo/Resources/Language.Designer.cs @@ -1715,6 +1715,51 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to ESC to clear. + /// + internal static string Keybind_EscToClear { + get { + return ResourceManager.GetString("Keybind_EscToClear", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alt. + /// + internal static string Keybind_Modifier_Alt { + get { + return ResourceManager.GetString("Keybind_Modifier_Alt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ctrl. + /// + internal static string Keybind_Modifier_Ctrl { + get { + return ResourceManager.GetString("Keybind_Modifier_Ctrl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shift. + /// + internal static string Keybind_Modifier_Shift { + get { + return ResourceManager.GetString("Keybind_Modifier_Shift", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to none set. + /// + internal static string Keybind_None { + get { + return ResourceManager.GetString("Keybind_None", resourceCulture); + } + } + /// /// Looks up a localized string similar to Flexible. /// @@ -1940,6 +1985,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Cycle chat tab backwards keybind. + /// + internal static string Options_ChatTabBackwardKeybind_Name { + get { + return ResourceManager.GetString("Options_ChatTabBackwardKeybind_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cycle chat tab forwards keybind. + /// + internal static string Options_ChatTabForwardKeybind_Name { + get { + return ResourceManager.GetString("Options_ChatTabForwardKeybind_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Clear the message history database. /// diff --git a/ChatTwo/Resources/Language.resx b/ChatTwo/Resources/Language.resx index ec6eb53..9018653 100644 --- a/ChatTwo/Resources/Language.resx +++ b/ChatTwo/Resources/Language.resx @@ -514,6 +514,27 @@ Styles + + Cycle chat tab forwards keybind + + + Cycle chat tab backwards keybind + + + none set + + + ESC to clear + + + Ctrl + + + Alt + + + Shift + Ctrl + {0} diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index 86991d2..c0d925f 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -41,6 +41,7 @@ public sealed class ChatLogWindow : Window internal Vector4 DefaultText { get; set; } + internal int? WantedTab { get; set; } internal Tab? CurrentTab { get @@ -348,6 +349,34 @@ public sealed class ChatLogWindow : Window if (ImGui.GetIO().KeyShift) modifierState |= ModifierFlag.Shift; + bool KeyPressed(VirtualKey vk, ModifierFlag modifier) + { + if (!vk.TryToImGui(out var key)) + return false; + + var modifierPressed = Plugin.Config.KeybindMode switch + { + KeybindMode.Strict => modifier == modifierState, + KeybindMode.Flexible => modifierState.HasFlag(modifier), + _ => false, + }; + + return ImGui.IsKeyPressed(key) && modifierPressed && (modifier != 0 || !modifiersOnly); + } + + // Test for custom keybinds for changing chat tabs before checking + // vanilla keybinds. + if (Plugin.Config.ChatTabBackward != null && KeyPressed(Plugin.Config.ChatTabBackward.Key, Plugin.Config.ChatTabBackward.Modifier)) + { + Plugin.ChatLogWindow.ChangeTabDelta(-1); + return; + } + if (Plugin.Config.ChatTabForward != null && KeyPressed(Plugin.Config.ChatTabForward.Key, Plugin.Config.ChatTabForward.Modifier)) + { + Plugin.ChatLogWindow.ChangeTabDelta(1); + return; + } + var turnedOff = new Dictionary(); foreach (var (toIntercept, keybind) in Plugin.Functions.Chat.Keybinds) { @@ -356,17 +385,7 @@ public sealed class ChatLogWindow : Window void Intercept(VirtualKey vk, ModifierFlag modifier) { - if (!vk.TryToImGui(out var key)) - return; - - var modifierPressed = Plugin.Config.KeybindMode switch - { - KeybindMode.Strict => modifier == modifierState, - KeybindMode.Flexible => modifierState.HasFlag(modifier), - _ => false, - }; - - if (!ImGui.IsKeyPressed(key) || !modifierPressed || modifier == 0 && modifiersOnly) + if (!KeyPressed(vk, modifier)) return; var bits = BitOperations.PopCount((uint) modifier); @@ -395,6 +414,16 @@ public sealed class ChatLogWindow : Window } } + internal void ChangeTab(int index) => WantedTab = index; + + internal void ChangeTabDelta(int offset) + { + var newIndex = (LastTab + offset) % Plugin.Config.Tabs.Count; + while (newIndex < 0) + newIndex += Plugin.Config.Tabs.Count; + ChangeTab(newIndex); + } + private void TabChannelSwitch(Tab tab) { // Save the previous channel to restore it later @@ -1158,7 +1187,10 @@ public sealed class ChatLogWindow : Window continue; var unread = tabI == LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; - using var tabItem = ImRaii.TabItem($"{tab.Name}{unread}###log-tab-{tabI}"); + var flags = ImGuiTabItemFlags.None; + if (WantedTab == tabI) + flags |= ImGuiTabItemFlags.SetSelected; + using var tabItem = ImRaii.TabItem($"{tab.Name}{unread}###log-tab-{tabI}", flags); DrawTabContextMenu(tab, tabI); if (!tabItem.Success) @@ -1174,6 +1206,7 @@ public sealed class ChatLogWindow : Window DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), switchedTab); } + WantedTab = null; return currentTab; } @@ -1203,10 +1236,10 @@ public sealed class ChatLogWindow : Window continue; var unread = tabI == LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; - var clicked = ImGui.Selectable($"{tab.Name}{unread}###log-tab-{tabI}", LastTab == tabI); + var clicked = ImGui.Selectable($"{tab.Name}{unread}###log-tab-{tabI}", LastTab == tabI || WantedTab == tabI); DrawTabContextMenu(tab, tabI); - if (!clicked) + if (!clicked && WantedTab != tabI) continue; currentTab = tabI; @@ -1229,6 +1262,7 @@ public sealed class ChatLogWindow : Window if (currentTab > -1) DrawMessageLog(Plugin.Config.Tabs[currentTab], PayloadHandler, childHeight, switchedTab); + WantedTab = null; return currentTab; } diff --git a/ChatTwo/Ui/SettingsTabs/ChatLog.cs b/ChatTwo/Ui/SettingsTabs/ChatLog.cs index 2948a5b..4f4a305 100644 --- a/ChatTwo/Ui/SettingsTabs/ChatLog.cs +++ b/ChatTwo/Ui/SettingsTabs/ChatLog.cs @@ -72,6 +72,18 @@ internal sealed class ChatLog : ISettingsTab ImGui.Separator(); ImGui.Spacing(); + ImGui.TextUnformatted(Language.Options_ChatTabForwardKeybind_Name); + ImGui.SetNextItemWidth(-1); + ImGuiUtil.KeybindInput("ChatTabForwardKeybind", ref Mutable.ChatTabForward); + + ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name); + ImGui.SetNextItemWidth(-1); + ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + ImGui.TextUnformatted(Language.Options_AdjustPosition_Name); var pos = Plugin.ChatLogWindow.LastWindowPos; ImGui.SetNextItemWidth(-1); diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs index 83aab61..4937854 100755 --- a/ChatTwo/Util/ImGuiUtil.cs +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -1,5 +1,7 @@ using System.Numerics; using System.Text; +using ChatTwo.GameFunctions.Types; +using ChatTwo.Resources; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; @@ -304,6 +306,74 @@ internal static class ImGuiUtil } } + internal static bool KeybindInput(string id, ref ConfigKeyBind? keybind) + { + var idUint = ImGui.GetID(id); + using var pushedId = ImRaii.PushId(id); + if (ImGui.GetStateStorage().GetBool(idUint)) + { + var io = ImGui.GetIO(); + var currentMods = ModifierFlag.None; + var modString = ""; + if (io.KeyCtrl) + { + currentMods |= ModifierFlag.Ctrl; + modString += Language.Keybind_Modifier_Ctrl + " + "; + } + if (io.KeyShift) + { + currentMods |= ModifierFlag.Shift; + modString += Language.Keybind_Modifier_Shift + " + "; + } + if (io.KeyAlt) + { + currentMods |= ModifierFlag.Alt; + modString += Language.Keybind_Modifier_Alt + " + "; + } + + var text = $"{modString}... ({Language.Keybind_EscToClear})"; + using (ImRaii.PushColor(ImGuiCol.TextSelectedBg, Vector4.Zero)) + { + ImGui.SetKeyboardFocusHere(); + ImGui.InputText(id + "##keybind", ref text, 0, ImGuiInputTextFlags.ReadOnly); + } + + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + { + keybind = null; + ImGui.GetStateStorage().SetBool(idUint, false); + return false; + } + + foreach (var vk in Enum.GetValues(typeof(VirtualKey)).Cast()) + { + if (vk is VirtualKey.NO_KEY or VirtualKey.CONTROL or VirtualKey.LCONTROL or VirtualKey.RCONTROL or VirtualKey.SHIFT or VirtualKey.LSHIFT or VirtualKey.RSHIFT or VirtualKey.MENU or VirtualKey.LMENU or VirtualKey.RMENU) + continue; + + if (!TryToImGui(vk, out var imKey) || !ImGui.IsKeyPressed(imKey)) + continue; + + keybind = new ConfigKeyBind + { + Modifier = currentMods, + Key = vk + }; + ImGui.GetStateStorage().SetBool(idUint, false); + return true; + } + } + else + { + var text = $"({Language.Keybind_None})"; + if (keybind != null) + text = keybind.ToString(); + if (ImGui.Button(text, new Vector2(-1, 0))) + ImGui.GetStateStorage().SetBool(idUint, true); + } + + return false; + } + public static void DrawArrows(ref int selected, int min, int max, float spacing, int id = 0) { // Prevents changing values from triggering EndDisable From fb167a81610c8c1ca3515151537b7bea3b163ea9 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 15 Jul 2024 19:16:30 +1000 Subject: [PATCH 2/2] chore: refactor keybinds to be in new KeybindManager --- ChatTwo/GameFunctions/Chat.cs | 238 +----------- ChatTwo/GameFunctions/GameFunctions.cs | 3 + ChatTwo/GameFunctions/KeybindManager.cs | 482 ++++++++++++++++++++++++ ChatTwo/Ui/ChatLogWindow.cs | 86 +---- 4 files changed, 494 insertions(+), 315 deletions(-) create mode 100644 ChatTwo/GameFunctions/KeybindManager.cs diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index d36b307..a6da4fa 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -1,15 +1,11 @@ -using System.Numerics; -using System.Text; +using System.Text; using ChatTwo.Code; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using ChatTwo.Util; -using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Config; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Memory; -using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.System.Framework; @@ -29,10 +25,6 @@ namespace ChatTwo.GameFunctions; internal sealed unsafe class Chat : IDisposable { // Functions - // Replace with - [Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")] - private readonly delegate* unmanaged GetKeybindNative = null!; - [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")] private readonly delegate* unmanaged PrintTellNative = null!; @@ -54,11 +46,6 @@ internal sealed unsafe class Chat : IDisposable [Signature("48 8D 35 ?? ?? ?? ?? 8B 05", ScanType = ScanType.StaticAddress)] private readonly char* CurrentCharacter = null!; - // Events - - internal event ChatActivatedEventDelegate? Activated; - internal delegate void ChatActivatedEventDelegate(ChatActivatedArgs args); - private Plugin Plugin { get; } /// @@ -70,9 +57,6 @@ internal sealed unsafe class Chat : IDisposable internal bool UsesTellTempChannel { get; set; } internal InputChannel? PreviousChannel { get; private set; } - private bool DirectChat; - private long LastRefresh; - internal Chat(Plugin plugin) { Plugin = plugin; @@ -92,7 +76,6 @@ internal sealed unsafe class Chat : IDisposable // EurekaContextMenuTellHook = Plugin.GameInteropProvider.HookFromAddress(RaptureShellModule.MemberFunctionPointers.SetContextTellTargetInForay, SetContextTellTargetInForay); // EurekaContextMenuTellHook.Enable(); - Plugin.Framework.Update += InterceptKeybinds; Plugin.ClientState.Login += Login; Login(); } @@ -100,15 +83,12 @@ internal sealed unsafe class Chat : IDisposable public void Dispose() { Plugin.ClientState.Login -= Login; - Plugin.Framework.Update -= InterceptKeybinds; SetChatLogTellTargetHook?.Dispose(); ReplyInSelectedChatModeHook?.Dispose(); ChangeChannelNameHook?.Dispose(); ChatLogRefreshHook?.Dispose(); EurekaContextMenuTellHook?.Dispose(); - - Activated = null; } internal string? GetLinkshellName(uint idx) @@ -164,182 +144,6 @@ internal sealed unsafe class Chat : IDisposable return 0xFF | (rgb << 8); } - private readonly Dictionary _keybinds = new(); - internal IReadOnlyDictionary Keybinds => _keybinds; - - internal static readonly IReadOnlyDictionary KeybindsToIntercept = new Dictionary - { - ["CMD_CHAT"] = new(null), - ["CMD_COMMAND"] = new(null, text: "/"), - ["CMD_REPLY"] = new(InputChannel.Tell, rotate: RotateMode.Forward), - ["CMD_REPLY_REV"] = new(InputChannel.Tell, rotate: RotateMode.Reverse), - ["CMD_SAY"] = new(InputChannel.Say), - ["CMD_YELL"] = new(InputChannel.Yell), - ["CMD_SHOUT"] = new(InputChannel.Shout), - ["CMD_PARTY"] = new(InputChannel.Party), - ["CMD_ALLIANCE"] = new(InputChannel.Alliance), - ["CMD_FREECOM"] = new(InputChannel.FreeCompany), - ["PVPTEAM_CHAT"] = new(InputChannel.PvpTeam), - ["CMD_CWLINKSHELL"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Forward), - ["CMD_CWLINKSHELL_REV"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Reverse), - ["CMD_CWLINKSHELL_1"] = new(InputChannel.CrossLinkshell1), - ["CMD_CWLINKSHELL_2"] = new(InputChannel.CrossLinkshell2), - ["CMD_CWLINKSHELL_3"] = new(InputChannel.CrossLinkshell3), - ["CMD_CWLINKSHELL_4"] = new(InputChannel.CrossLinkshell4), - ["CMD_CWLINKSHELL_5"] = new(InputChannel.CrossLinkshell5), - ["CMD_CWLINKSHELL_6"] = new(InputChannel.CrossLinkshell6), - ["CMD_CWLINKSHELL_7"] = new(InputChannel.CrossLinkshell7), - ["CMD_CWLINKSHELL_8"] = new(InputChannel.CrossLinkshell8), - ["CMD_LINKSHELL"] = new(InputChannel.Linkshell1, rotate: RotateMode.Forward), - ["CMD_LINKSHELL_REV"] = new(InputChannel.Linkshell1, rotate: RotateMode.Reverse), - ["CMD_LINKSHELL_1"] = new(InputChannel.Linkshell1), - ["CMD_LINKSHELL_2"] = new(InputChannel.Linkshell2), - ["CMD_LINKSHELL_3"] = new(InputChannel.Linkshell3), - ["CMD_LINKSHELL_4"] = new(InputChannel.Linkshell4), - ["CMD_LINKSHELL_5"] = new(InputChannel.Linkshell5), - ["CMD_LINKSHELL_6"] = new(InputChannel.Linkshell6), - ["CMD_LINKSHELL_7"] = new(InputChannel.Linkshell7), - ["CMD_LINKSHELL_8"] = new(InputChannel.Linkshell8), - ["CMD_BEGINNER"] = new(InputChannel.NoviceNetwork), - ["CMD_REPLY_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Forward), - ["CMD_REPLY_REV_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Reverse), - ["CMD_SAY_ALWAYS"] = new(InputChannel.Say, true), - ["CMD_YELL_ALWAYS"] = new(InputChannel.Yell, true), - ["CMD_PARTY_ALWAYS"] = new(InputChannel.Party, true), - ["CMD_ALLIANCE_ALWAYS"] = new(InputChannel.Alliance, true), - ["CMD_FREECOM_ALWAYS"] = new(InputChannel.FreeCompany, true), - ["PVPTEAM_CHAT_ALWAYS"] = new(InputChannel.PvpTeam, true), - ["CMD_CWLINKSHELL_ALWAYS"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Forward), - ["CMD_CWLINKSHELL_ALWAYS_REV"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Reverse), - ["CMD_CWLINKSHELL_1_ALWAYS"] = new(InputChannel.CrossLinkshell1, true), - ["CMD_CWLINKSHELL_2_ALWAYS"] = new(InputChannel.CrossLinkshell2, true), - ["CMD_CWLINKSHELL_3_ALWAYS"] = new(InputChannel.CrossLinkshell3, true), - ["CMD_CWLINKSHELL_4_ALWAYS"] = new(InputChannel.CrossLinkshell4, true), - ["CMD_CWLINKSHELL_5_ALWAYS"] = new(InputChannel.CrossLinkshell5, true), - ["CMD_CWLINKSHELL_6_ALWAYS"] = new(InputChannel.CrossLinkshell6, true), - ["CMD_CWLINKSHELL_7_ALWAYS"] = new(InputChannel.CrossLinkshell7, true), - ["CMD_CWLINKSHELL_8_ALWAYS"] = new(InputChannel.CrossLinkshell8, true), - ["CMD_LINKSHELL_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Forward), - ["CMD_LINKSHELL_REV_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Reverse), - ["CMD_LINKSHELL_1_ALWAYS"] = new(InputChannel.Linkshell1, true), - ["CMD_LINKSHELL_2_ALWAYS"] = new(InputChannel.Linkshell2, true), - ["CMD_LINKSHELL_3_ALWAYS"] = new(InputChannel.Linkshell3, true), - ["CMD_LINKSHELL_4_ALWAYS"] = new(InputChannel.Linkshell4, true), - ["CMD_LINKSHELL_5_ALWAYS"] = new(InputChannel.Linkshell5, true), - ["CMD_LINKSHELL_6_ALWAYS"] = new(InputChannel.Linkshell6, true), - ["CMD_LINKSHELL_7_ALWAYS"] = new(InputChannel.Linkshell7, true), - ["CMD_LINKSHELL_8_ALWAYS"] = new(InputChannel.Linkshell8, true), - ["CMD_BEGINNER_ALWAYS"] = new(InputChannel.NoviceNetwork, true), - }; - - private void UpdateKeybinds() - { - foreach (var name in KeybindsToIntercept.Keys) - { - var keybind = GetKeybind(name); - if (keybind is null) - continue; - - _keybinds[name] = keybind; - } - } - - private void InterceptKeybinds(IFramework framework1) - { - // Refresh current keybinds every 5s - if (LastRefresh + 5 * 1000 < Environment.TickCount64) - { - UpdateKeybinds(); - DirectChat = Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option; - LastRefresh = Environment.TickCount64; - } - - // Vanilla text input has focus - if (RaptureAtkModule.Instance()->AtkModule.IsTextInputActive()) - return; - - var modifierState = (ModifierFlag) 0; - foreach (var modifier in Enum.GetValues()) - { - var modifierKey = GetKeyForModifier(modifier); - if (modifierKey != VirtualKey.NO_KEY && Plugin.KeyState[modifierKey]) - modifierState |= modifier; - } - - bool KeyPressed(VirtualKey key, ModifierFlag modifier) - { - if (!Plugin.KeyState.IsVirtualKeyValid(key)) - return false; - - var modifierPressed = Plugin.Config.KeybindMode switch - { - KeybindMode.Strict => modifier == modifierState, - KeybindMode.Flexible => modifierState.HasFlag(modifier), - _ => false, - }; - - return modifierPressed && Plugin.KeyState[key]; - } - - // Test for custom keybinds for changing chat tabs before checking - // vanilla keybinds. - if (Plugin.Config.ChatTabBackward != null && KeyPressed(Plugin.Config.ChatTabBackward.Key, Plugin.Config.ChatTabBackward.Modifier)) - { - Plugin.KeyState[Plugin.Config.ChatTabBackward.Key] = false; - Plugin.ChatLogWindow.ChangeTabDelta(-1); - return; - } - if (Plugin.Config.ChatTabForward != null && KeyPressed(Plugin.Config.ChatTabForward.Key, Plugin.Config.ChatTabForward.Modifier)) - { - Plugin.KeyState[Plugin.Config.ChatTabForward.Key] = false; - Plugin.ChatLogWindow.ChangeTabDelta(1); - return; - } - - var turnedOff = new Dictionary(); - foreach (var toIntercept in KeybindsToIntercept.Keys) - { - if (!Keybinds.TryGetValue(toIntercept, out var keybind)) - continue; - - if (toIntercept is "CMD_CHAT" or "CMD_COMMAND") - { - // Direct chat option is selected - if (DirectChat) - continue; - } - - void Intercept(VirtualKey key, ModifierFlag modifier) - { - if (!KeyPressed(key, modifier)) - return; - - var bits = BitOperations.PopCount((uint) modifier); - if (!turnedOff.TryGetValue(key, out var previousBits) || previousBits.Item1 < bits) - turnedOff[key] = ((uint) bits, toIntercept); - } - - Intercept(keybind.Key1, keybind.Modifier1); - Intercept(keybind.Key2, keybind.Modifier2); - } - - foreach (var (key, (_, keybind)) in turnedOff) - { - Plugin.KeyState[key] = false; - if (!KeybindsToIntercept.TryGetValue(keybind, out var info)) - continue; - - try - { - Activated?.Invoke(new ChatActivatedArgs(info) { TellReason = TellReason.Reply, }); - } - catch (Exception ex) - { - Plugin.Log.Error(ex, "Error in chat Activated event"); - } - } - } - private void Login() { var agent = AgentChatLog.Instance(); @@ -357,7 +161,7 @@ internal sealed unsafe class Chat : IDisposable if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) return ChatLogRefreshHook!.Original(log, eventId, value); - if (DirectChat && CurrentCharacter != null) + if (Plugin.Functions.KeybindManager.DirectChat && CurrentCharacter != null) { // FIXME: this whole system sucks // FIXME v2: I hate everything about this, but it works @@ -376,7 +180,7 @@ internal sealed unsafe class Chat : IDisposable try { - Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, }); + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, }); } catch (Exception ex) { @@ -397,7 +201,7 @@ internal sealed unsafe class Chat : IDisposable try { - Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, }); + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, }); } catch (Exception ex) { @@ -463,7 +267,7 @@ internal sealed unsafe class Chat : IDisposable try { var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason); - Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell)) + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell)) { TellReason = (TellReason) reason, TellTarget = target, @@ -547,38 +351,6 @@ internal sealed unsafe class Chat : IDisposable utfWorld->Dtor(true); } - private static VirtualKey GetKeyForModifier(ModifierFlag modifierFlag) => modifierFlag switch - { - ModifierFlag.Shift => VirtualKey.SHIFT, - ModifierFlag.Ctrl => VirtualKey.CONTROL, - ModifierFlag.Alt => VirtualKey.MENU, - _ => VirtualKey.NO_KEY, - }; - - private Keybind GetKeybind(string id) - { - var outData = stackalloc byte[11]; - var idString = Utf8String.FromString(id); - GetKeybindNative(UIInputData.Instance(), idString, (nint) outData); - idString->Dtor(true); - - var key1 = (VirtualKey) outData[0]; - if (key1 is VirtualKey.F23) - key1 = VirtualKey.OEM_2; - - var key2 = (VirtualKey) outData[2]; - if (key2 is VirtualKey.F23) - key2 = VirtualKey.OEM_2; - - return new Keybind - { - Key1 = key1, - Modifier1 = (ModifierFlag) outData[1], - Key2 = key2, - Modifier2 = (ModifierFlag) outData[3], - }; - } - internal TellHistoryInfo? GetTellHistoryInfo(int index) { var acquaintance = AcquaintanceModule.Instance()->GetTellHistory(index); diff --git a/ChatTwo/GameFunctions/GameFunctions.cs b/ChatTwo/GameFunctions/GameFunctions.cs index 8de4a52..59200fe 100755 --- a/ChatTwo/GameFunctions/GameFunctions.cs +++ b/ChatTwo/GameFunctions/GameFunctions.cs @@ -25,11 +25,13 @@ internal unsafe class GameFunctions : IDisposable #endregion private Plugin Plugin { get; } + internal KeybindManager KeybindManager { get; } internal Chat Chat { get; } internal GameFunctions(Plugin plugin) { Plugin = plugin; + KeybindManager = new KeybindManager(plugin); Chat = new Chat(Plugin); Plugin.GameInteropProvider.InitializeFromAttributes(this); @@ -40,6 +42,7 @@ internal unsafe class GameFunctions : IDisposable public void Dispose() { Chat.Dispose(); + KeybindManager.Dispose(); ResolveTextCommandPlaceholderHook?.Dispose(); diff --git a/ChatTwo/GameFunctions/KeybindManager.cs b/ChatTwo/GameFunctions/KeybindManager.cs new file mode 100644 index 0000000..d83beea --- /dev/null +++ b/ChatTwo/GameFunctions/KeybindManager.cs @@ -0,0 +1,482 @@ +using System.Numerics; +using ChatTwo.Code; +using ChatTwo.GameFunctions.Types; +using ChatTwo.Util; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Config; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using ImGuiNET; + +namespace ChatTwo.GameFunctions; + +internal enum KeyboardSource { + Game, + ImGui +} + +internal unsafe class KeybindManager : IDisposable { + // Functions + // Replace with + [Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")] + private readonly delegate* unmanaged GetKeybindNative = null!; + + private Plugin Plugin { get; } + + internal bool DirectChat; + private long LastRefresh; + + private readonly Dictionary Keybinds = new(); + private static readonly IReadOnlyDictionary KeybindsToIntercept = new Dictionary + { + ["CMD_CHAT"] = new(null), + ["CMD_COMMAND"] = new(null, text: "/"), + ["CMD_REPLY"] = new(InputChannel.Tell, rotate: RotateMode.Forward), + ["CMD_REPLY_REV"] = new(InputChannel.Tell, rotate: RotateMode.Reverse), + ["CMD_SAY"] = new(InputChannel.Say), + ["CMD_YELL"] = new(InputChannel.Yell), + ["CMD_SHOUT"] = new(InputChannel.Shout), + ["CMD_PARTY"] = new(InputChannel.Party), + ["CMD_ALLIANCE"] = new(InputChannel.Alliance), + ["CMD_FREECOM"] = new(InputChannel.FreeCompany), + ["PVPTEAM_CHAT"] = new(InputChannel.PvpTeam), + ["CMD_CWLINKSHELL"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Forward), + ["CMD_CWLINKSHELL_REV"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Reverse), + ["CMD_CWLINKSHELL_1"] = new(InputChannel.CrossLinkshell1), + ["CMD_CWLINKSHELL_2"] = new(InputChannel.CrossLinkshell2), + ["CMD_CWLINKSHELL_3"] = new(InputChannel.CrossLinkshell3), + ["CMD_CWLINKSHELL_4"] = new(InputChannel.CrossLinkshell4), + ["CMD_CWLINKSHELL_5"] = new(InputChannel.CrossLinkshell5), + ["CMD_CWLINKSHELL_6"] = new(InputChannel.CrossLinkshell6), + ["CMD_CWLINKSHELL_7"] = new(InputChannel.CrossLinkshell7), + ["CMD_CWLINKSHELL_8"] = new(InputChannel.CrossLinkshell8), + ["CMD_LINKSHELL"] = new(InputChannel.Linkshell1, rotate: RotateMode.Forward), + ["CMD_LINKSHELL_REV"] = new(InputChannel.Linkshell1, rotate: RotateMode.Reverse), + ["CMD_LINKSHELL_1"] = new(InputChannel.Linkshell1), + ["CMD_LINKSHELL_2"] = new(InputChannel.Linkshell2), + ["CMD_LINKSHELL_3"] = new(InputChannel.Linkshell3), + ["CMD_LINKSHELL_4"] = new(InputChannel.Linkshell4), + ["CMD_LINKSHELL_5"] = new(InputChannel.Linkshell5), + ["CMD_LINKSHELL_6"] = new(InputChannel.Linkshell6), + ["CMD_LINKSHELL_7"] = new(InputChannel.Linkshell7), + ["CMD_LINKSHELL_8"] = new(InputChannel.Linkshell8), + ["CMD_BEGINNER"] = new(InputChannel.NoviceNetwork), + ["CMD_REPLY_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Forward), + ["CMD_REPLY_REV_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Reverse), + ["CMD_SAY_ALWAYS"] = new(InputChannel.Say, true), + ["CMD_YELL_ALWAYS"] = new(InputChannel.Yell, true), + ["CMD_PARTY_ALWAYS"] = new(InputChannel.Party, true), + ["CMD_ALLIANCE_ALWAYS"] = new(InputChannel.Alliance, true), + ["CMD_FREECOM_ALWAYS"] = new(InputChannel.FreeCompany, true), + ["PVPTEAM_CHAT_ALWAYS"] = new(InputChannel.PvpTeam, true), + ["CMD_CWLINKSHELL_ALWAYS"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Forward), + ["CMD_CWLINKSHELL_ALWAYS_REV"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Reverse), + ["CMD_CWLINKSHELL_1_ALWAYS"] = new(InputChannel.CrossLinkshell1, true), + ["CMD_CWLINKSHELL_2_ALWAYS"] = new(InputChannel.CrossLinkshell2, true), + ["CMD_CWLINKSHELL_3_ALWAYS"] = new(InputChannel.CrossLinkshell3, true), + ["CMD_CWLINKSHELL_4_ALWAYS"] = new(InputChannel.CrossLinkshell4, true), + ["CMD_CWLINKSHELL_5_ALWAYS"] = new(InputChannel.CrossLinkshell5, true), + ["CMD_CWLINKSHELL_6_ALWAYS"] = new(InputChannel.CrossLinkshell6, true), + ["CMD_CWLINKSHELL_7_ALWAYS"] = new(InputChannel.CrossLinkshell7, true), + ["CMD_CWLINKSHELL_8_ALWAYS"] = new(InputChannel.CrossLinkshell8, true), + ["CMD_LINKSHELL_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Forward), + ["CMD_LINKSHELL_REV_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Reverse), + ["CMD_LINKSHELL_1_ALWAYS"] = new(InputChannel.Linkshell1, true), + ["CMD_LINKSHELL_2_ALWAYS"] = new(InputChannel.Linkshell2, true), + ["CMD_LINKSHELL_3_ALWAYS"] = new(InputChannel.Linkshell3, true), + ["CMD_LINKSHELL_4_ALWAYS"] = new(InputChannel.Linkshell4, true), + ["CMD_LINKSHELL_5_ALWAYS"] = new(InputChannel.Linkshell5, true), + ["CMD_LINKSHELL_6_ALWAYS"] = new(InputChannel.Linkshell6, true), + ["CMD_LINKSHELL_7_ALWAYS"] = new(InputChannel.Linkshell7, true), + ["CMD_LINKSHELL_8_ALWAYS"] = new(InputChannel.Linkshell8, true), + ["CMD_BEGINNER_ALWAYS"] = new(InputChannel.NoviceNetwork, true) + }; + + // List of keys that can be used as a part of keybinds while the chat is + // focused WITHOUT modifiers. All other keys can only be used if their + // configured keybind contains modifiers. This allows for using e.g. F11 to + // change chat channel while typing. + private static readonly IReadOnlyCollection ModifierlessChatKeys = new[] + { + // VirtualKey.NO_KEY, + VirtualKey.LBUTTON, + VirtualKey.RBUTTON, + VirtualKey.CANCEL, + VirtualKey.MBUTTON, + VirtualKey.XBUTTON1, + VirtualKey.XBUTTON2, + VirtualKey.BACK, + VirtualKey.TAB, + VirtualKey.CLEAR, + // VirtualKey.RETURN, // handled by imgui + // VirtualKey.SHIFT, + // VirtualKey.CONTROL, + // VirtualKey.MENU, + VirtualKey.PAUSE, + // VirtualKey.CAPITAL, + // VirtualKey.KANA, + // VirtualKey.HANGUL, + // VirtualKey.JUNJA, + // VirtualKey.FINAL, + // VirtualKey.HANJA, + // VirtualKey.KANJI, + VirtualKey.ESCAPE, + // VirtualKey.CONVERT, + // VirtualKey.NONCONVERT, + // VirtualKey.ACCEPT, + // VirtualKey.MODECHANGE, + // VirtualKey.SPACE, + VirtualKey.PRIOR, + VirtualKey.NEXT, + VirtualKey.END, + VirtualKey.HOME, + // VirtualKey.LEFT, // handled by imgui + VirtualKey.UP, + // VirtualKey.RIGHT, // handled by imgui + VirtualKey.DOWN, + VirtualKey.SELECT, + VirtualKey.PRINT, + VirtualKey.EXECUTE, + VirtualKey.SNAPSHOT, + VirtualKey.INSERT, + VirtualKey.DELETE, + VirtualKey.HELP, + // VirtualKey.KEY_0, + // VirtualKey.KEY_1, + // VirtualKey.KEY_2, + // VirtualKey.KEY_3, + // VirtualKey.KEY_4, + // VirtualKey.KEY_5, + // VirtualKey.KEY_6, + // VirtualKey.KEY_7, + // VirtualKey.KEY_8, + // VirtualKey.KEY_9, + // VirtualKey.A, + // VirtualKey.B, + // VirtualKey.C, + // VirtualKey.D, + // VirtualKey.E, + // VirtualKey.F, + // VirtualKey.G, + // VirtualKey.H, + // VirtualKey.I, + // VirtualKey.J, + // VirtualKey.K, + // VirtualKey.L, + // VirtualKey.M, + // VirtualKey.N, + // VirtualKey.O, + // VirtualKey.P, + // VirtualKey.Q, + // VirtualKey.R, + // VirtualKey.S, + // VirtualKey.T, + // VirtualKey.U, + // VirtualKey.V, + // VirtualKey.W, + // VirtualKey.X, + // VirtualKey.Y, + // VirtualKey.Z, + // VirtualKey.LWIN, + // VirtualKey.RWIN, + VirtualKey.APPS, + VirtualKey.SLEEP, + // VirtualKey.NUMPAD0, + // VirtualKey.NUMPAD1, + // VirtualKey.NUMPAD2, + // VirtualKey.NUMPAD3, + // VirtualKey.NUMPAD4, + // VirtualKey.NUMPAD5, + // VirtualKey.NUMPAD6, + // VirtualKey.NUMPAD7, + // VirtualKey.NUMPAD8, + // VirtualKey.NUMPAD9, + // VirtualKey.MULTIPLY, + // VirtualKey.ADD, + // VirtualKey.SEPARATOR, + // VirtualKey.SUBTRACT, + // VirtualKey.DECIMAL, + // VirtualKey.DIVIDE, + VirtualKey.F1, + VirtualKey.F2, + VirtualKey.F3, + VirtualKey.F4, + VirtualKey.F5, + VirtualKey.F6, + VirtualKey.F7, + VirtualKey.F8, + VirtualKey.F9, + VirtualKey.F10, + VirtualKey.F11, + VirtualKey.F12, + VirtualKey.F13, + VirtualKey.F14, + VirtualKey.F15, + VirtualKey.F16, + VirtualKey.F17, + VirtualKey.F18, + VirtualKey.F19, + VirtualKey.F20, + VirtualKey.F21, + VirtualKey.F22, + VirtualKey.F23, + VirtualKey.F24, + VirtualKey.NUMLOCK, + VirtualKey.SCROLL, + // VirtualKey.OEM_FJ_JISHO, + // VirtualKey.OEM_NEC_EQUAL, + // VirtualKey.OEM_FJ_MASSHOU, + // VirtualKey.OEM_FJ_TOUROKU, + // VirtualKey.OEM_FJ_LOYA, + // VirtualKey.OEM_FJ_ROYA, + // VirtualKey.LSHIFT, + // VirtualKey.RSHIFT, + // VirtualKey.LCONTROL, + // VirtualKey.RCONTROL, + // VirtualKey.LMENU, + // VirtualKey.RMENU, + VirtualKey.BROWSER_BACK, + VirtualKey.BROWSER_FORWARD, + VirtualKey.BROWSER_REFRESH, + VirtualKey.BROWSER_STOP, + VirtualKey.BROWSER_SEARCH, + VirtualKey.BROWSER_FAVORITES, + VirtualKey.BROWSER_HOME, + VirtualKey.VOLUME_MUTE, + VirtualKey.VOLUME_DOWN, + VirtualKey.VOLUME_UP, + VirtualKey.MEDIA_NEXT_TRACK, + VirtualKey.MEDIA_PREV_TRACK, + VirtualKey.MEDIA_STOP, + VirtualKey.MEDIA_PLAY_PAUSE, + VirtualKey.LAUNCH_MAIL, + VirtualKey.LAUNCH_MEDIA_SELECT, + VirtualKey.LAUNCH_APP1, + VirtualKey.LAUNCH_APP2, + // VirtualKey.OEM_1, + // VirtualKey.OEM_PLUS, + // VirtualKey.OEM_COMMA, + // VirtualKey.OEM_MINUS, + // VirtualKey.OEM_PERIOD, + // VirtualKey.OEM_2, + // VirtualKey.OEM_3, + // VirtualKey.OEM_4, // [{ + // VirtualKey.OEM_5, // \" + // VirtualKey.OEM_6, // ]} + // VirtualKey.OEM_7, // '" + // VirtualKey.OEM_8, + // VirtualKey.OEM_AX, + // VirtualKey.OEM_102, + // VirtualKey.ICO_HELP, + // VirtualKey.ICO_00, + // VirtualKey.PROCESSKEY, + // VirtualKey.ICO_CLEAR, + // VirtualKey.PACKET, + // VirtualKey.OEM_RESET, + // VirtualKey.OEM_JUMP, + // VirtualKey.OEM_PA1, + // VirtualKey.OEM_PA2, + // VirtualKey.OEM_PA3, + // VirtualKey.OEM_WSCTRL, + // VirtualKey.OEM_CUSEL, + // VirtualKey.OEM_ATTN, + // VirtualKey.OEM_FINISH, + // VirtualKey.OEM_COPY, + // VirtualKey.OEM_AUTO, + // VirtualKey.OEM_ENLW, + // VirtualKey.OEM_BACKTAB, + // VirtualKey.ATTN, + // VirtualKey.CRSEL, + // VirtualKey.EXSEL, + // VirtualKey.EREOF, + // VirtualKey.PLAY, + // VirtualKey.ZOOM, + // VirtualKey.NONAME, + // VirtualKey.PA1, + // VirtualKey.OEM_CLEAR, + }; + + internal KeybindManager(Plugin plugin) + { + Plugin = plugin; + Plugin.GameInteropProvider.InitializeFromAttributes(this); + + // Handle keybinds from the game on every tick. + Plugin.Framework.Update += HandleKeybinds; + } + + public void Dispose() + { + Plugin.Framework.Update -= HandleKeybinds; + } + + private void UpdateKeybinds() + { + foreach (var name in KeybindsToIntercept.Keys) + { + Keybinds[name] = GetKeybind(name); + } + } + + private static ModifierFlag GetModifiers(KeyboardSource source) + { + var modifierState = ModifierFlag.None; + if (source == KeyboardSource.Game) + { + if (Plugin.KeyState[VirtualKey.MENU]) + modifierState |= ModifierFlag.Alt; + if (Plugin.KeyState[VirtualKey.CONTROL]) + modifierState |= ModifierFlag.Ctrl; + if (Plugin.KeyState[VirtualKey.SHIFT]) + modifierState |= ModifierFlag.Shift; + return modifierState; + } + + if (ImGui.GetIO().KeyAlt) + modifierState |= ModifierFlag.Alt; + if (ImGui.GetIO().KeyCtrl) + modifierState |= ModifierFlag.Ctrl; + if (ImGui.GetIO().KeyShift) + modifierState |= ModifierFlag.Shift; + + return modifierState; + } + + private static bool KeyPressed(KeyboardSource source, VirtualKey key) + { + if (key == VirtualKey.NO_KEY) + return false; + if (source == KeyboardSource.Game) + return Plugin.KeyState[key]; + + return key.TryToImGui(out var imguiKey) && ImGui.IsKeyPressed(imguiKey); + } + + private static bool ComboPressed(KeyboardSource source, VirtualKey key, ModifierFlag modifier, ModifierFlag? modifierState = null, bool modifiersOnly = false) + { + // When we're in an input, we don't want to process any keybinds that + // don't have a modifier (or only use shift) and are not explicitly + // whitelisted. + if (modifiersOnly && !ModifierlessChatKeys.Contains(key) && modifier is ModifierFlag.None or ModifierFlag.Shift) + return false; + + modifierState ??= GetModifiers(source); + var modifierPressed = Plugin.Config.KeybindMode switch + { + KeybindMode.Strict => modifier == modifierState.Value, + KeybindMode.Flexible => modifierState.Value.HasFlag(modifier), + _ => false + }; + + return KeyPressed(source, key) && modifierPressed; + } + + private static bool ConfigKeybindPressed(KeyboardSource source, ConfigKeyBind? bind, ModifierFlag? modifierState = null, bool modifiersOnly = false) + { + return bind != null && ComboPressed(source, bind.Key, bind.Modifier, modifierState: modifierState, modifiersOnly: modifiersOnly); + } + + private void HandleKeybinds(IFramework _ ) => HandleKeybinds(KeyboardSource.Game); + + internal void HandleKeybinds(KeyboardSource source, bool ignoreChatOpen = false, bool modifiersOnly = false) + { + // Refresh current keybinds every 5s + if (LastRefresh + 5 * 1000 < Environment.TickCount64) + { + UpdateKeybinds(); + DirectChat = Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option; + LastRefresh = Environment.TickCount64; + } + + // Vanilla text input has focus + if (RaptureAtkModule.Instance()->AtkModule.IsTextInputActive()) + return; + + var modifierState = GetModifiers(source); + + // Test for custom keybinds for changing chat tabs before checking + // vanilla keybinds. + if (ConfigKeybindPressed(source, Plugin.Config.ChatTabForward)) + { + Plugin.KeyState[Plugin.Config.ChatTabForward!.Key] = false; + Plugin.ChatLogWindow.ChangeTabDelta(1); + return; + } + if (ConfigKeybindPressed(source, Plugin.Config.ChatTabBackward)) + { + Plugin.KeyState[Plugin.Config.ChatTabBackward!.Key] = false; + Plugin.ChatLogWindow.ChangeTabDelta(-1); + return; + } + + // Only process the active combo with the most modifiers. + var currentBest = (VirtualKey.NO_KEY, "", 0); + foreach (var (toIntercept, keybind) in Keybinds) + { + if (toIntercept is "CMD_CHAT" or "CMD_COMMAND" && (ignoreChatOpen || DirectChat)) + continue; + + void Intercept(VirtualKey vk, ModifierFlag modifier) + { + if (!ComboPressed(source, vk, modifier, modifierState: modifierState, modifiersOnly: modifiersOnly)) + return; + + var bits = BitOperations.PopCount((uint) modifier); + if (bits < currentBest.Item3) + return; + + currentBest = (vk, toIntercept, bits); + } + + Intercept(keybind.Key1, keybind.Modifier1); + Intercept(keybind.Key2, keybind.Modifier2); + } + + if (currentBest.Item1 == VirtualKey.NO_KEY) + return; + + Plugin.KeyState[currentBest.Item1] = false; + if (!KeybindsToIntercept.TryGetValue(currentBest.Item2, out var info)) + return; + + try + { + TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null; + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(info) { TellReason = reason, }); + } + catch (Exception ex) + { + Plugin.Log.Error(ex, "Error in chat Activated event"); + } + } + + private Keybind GetKeybind(string id) + { + var outData = stackalloc byte[11]; + var idString = Utf8String.FromString(id); + GetKeybindNative(UIInputData.Instance(), idString, (nint) outData); + idString->Dtor(true); + + var key1 = RemapInvalidVirtualKey((VirtualKey) outData[0]); + var key2 = RemapInvalidVirtualKey((VirtualKey) outData[2]); + return new Keybind + { + Key1 = key1, + Modifier1 = (ModifierFlag) outData[1], + Key2 = key2, + Modifier2 = (ModifierFlag) outData[3], + }; + } + + private static VirtualKey RemapInvalidVirtualKey(VirtualKey key) + { + return key switch + { + VirtualKey.F23 => VirtualKey.OEM_2, // /? + (VirtualKey) 140 => VirtualKey.OEM_7, // '" + _ => key + }; + } +} \ No newline at end of file diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index c0d925f..e522c32 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -3,12 +3,12 @@ using System.Numerics; using System.Runtime.InteropServices; using System.Text; using ChatTwo.Code; +using ChatTwo.GameFunctions; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using ChatTwo.Util; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; @@ -121,7 +121,6 @@ public sealed class ChatLogWindow : Window TextCommandSheet = Plugin.DataManager.GetExcelSheet()!; FontIcon = Plugin.TextureProvider.CreateFromTexFile(Plugin.DataManager.GetFile("common/font/fonticon_ps5.tex")!); - Plugin.Functions.Chat.Activated += Activated; Plugin.ClientState.Login += Login; Plugin.ClientState.Logout += Logout; @@ -133,7 +132,6 @@ public sealed class ChatLogWindow : Window Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip); Plugin.ClientState.Logout -= Logout; Plugin.ClientState.Login -= Login; - Plugin.Functions.Chat.Activated -= Activated; FontIcon?.Dispose(); Plugin.Commands.Register("/chat2").Execute -= ToggleChat; Plugin.Commands.Register("/clearlog2").Execute -= ClearLog; @@ -149,7 +147,7 @@ public sealed class ChatLogWindow : Window Plugin.MessageManager.FilterAllTabsAsync(); } - private void Activated(ChatActivatedArgs args) + internal void Activated(ChatActivatedArgs args) { Activate = true; PlayedClosingSound = false; @@ -337,83 +335,6 @@ public sealed class ChatLogWindow : Window return height; } - private void HandleKeybinds(bool modifiersOnly = false) - { - var modifierState = (ModifierFlag) 0; - if (ImGui.GetIO().KeyAlt) - modifierState |= ModifierFlag.Alt; - - if (ImGui.GetIO().KeyCtrl) - modifierState |= ModifierFlag.Ctrl; - - if (ImGui.GetIO().KeyShift) - modifierState |= ModifierFlag.Shift; - - bool KeyPressed(VirtualKey vk, ModifierFlag modifier) - { - if (!vk.TryToImGui(out var key)) - return false; - - var modifierPressed = Plugin.Config.KeybindMode switch - { - KeybindMode.Strict => modifier == modifierState, - KeybindMode.Flexible => modifierState.HasFlag(modifier), - _ => false, - }; - - return ImGui.IsKeyPressed(key) && modifierPressed && (modifier != 0 || !modifiersOnly); - } - - // Test for custom keybinds for changing chat tabs before checking - // vanilla keybinds. - if (Plugin.Config.ChatTabBackward != null && KeyPressed(Plugin.Config.ChatTabBackward.Key, Plugin.Config.ChatTabBackward.Modifier)) - { - Plugin.ChatLogWindow.ChangeTabDelta(-1); - return; - } - if (Plugin.Config.ChatTabForward != null && KeyPressed(Plugin.Config.ChatTabForward.Key, Plugin.Config.ChatTabForward.Modifier)) - { - Plugin.ChatLogWindow.ChangeTabDelta(1); - return; - } - - var turnedOff = new Dictionary(); - foreach (var (toIntercept, keybind) in Plugin.Functions.Chat.Keybinds) - { - if (toIntercept is "CMD_CHAT" or "CMD_COMMAND") - continue; - - void Intercept(VirtualKey vk, ModifierFlag modifier) - { - if (!KeyPressed(vk, modifier)) - return; - - var bits = BitOperations.PopCount((uint) modifier); - if (!turnedOff.TryGetValue(vk, out var previousBits) || previousBits.Item1 < bits) - turnedOff[vk] = ((uint) bits, toIntercept); - } - - Intercept(keybind.Key1, keybind.Modifier1); - Intercept(keybind.Key2, keybind.Modifier2); - } - - foreach (var (_, (_, keybind)) in turnedOff) - { - if (!GameFunctions.Chat.KeybindsToIntercept.TryGetValue(keybind, out var info)) - continue; - - try - { - TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null; - Activated(new ChatActivatedArgs(info) { TellReason = reason, }); - } - catch (Exception ex) - { - Plugin.Log.Error(ex, "Error in chat Activated event"); - } - } - } - internal void ChangeTab(int index) => WantedTab = index; internal void ChangeTabDelta(int offset) @@ -801,9 +722,10 @@ public sealed class ChatLogWindow : Window } } + // Process keybinds that have modifiers while the chat is focused. if (ImGui.IsItemActive()) { - HandleKeybinds(true); + Plugin.Functions.KeybindManager.HandleKeybinds(KeyboardSource.ImGui, true, true); LastActivityTime = FrameTime; }