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..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,165 +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; - } - - 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 (!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]) - 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(); @@ -340,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 @@ -359,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) { @@ -380,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) { @@ -446,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, @@ -530,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/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..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; @@ -41,6 +41,7 @@ public sealed class ChatLogWindow : Window internal Vector4 DefaultText { get; set; } + internal int? WantedTab { get; set; } internal Tab? CurrentTab { get @@ -120,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; @@ -132,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; @@ -148,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; @@ -336,63 +335,14 @@ public sealed class ChatLogWindow : Window return height; } - private void HandleKeybinds(bool modifiersOnly = false) + internal void ChangeTab(int index) => WantedTab = index; + + internal void ChangeTabDelta(int offset) { - 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; - - 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 (!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) - 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"); - } - } + var newIndex = (LastTab + offset) % Plugin.Config.Tabs.Count; + while (newIndex < 0) + newIndex += Plugin.Config.Tabs.Count; + ChangeTab(newIndex); } private void TabChannelSwitch(Tab tab) @@ -772,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; } @@ -1158,7 +1109,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 +1128,7 @@ public sealed class ChatLogWindow : Window DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), switchedTab); } + WantedTab = null; return currentTab; } @@ -1203,10 +1158,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 +1184,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