Merge pull request #82 from deansheather/dean/cycle-tabs-hotkeys

feat: add configurable hotkeys to cycle tabs
This commit is contained in:
Infi
2024-07-15 11:56:05 +02:00
committed by GitHub
10 changed files with 703 additions and 280 deletions
+26
View File
@@ -1,12 +1,33 @@
using System.Collections; using System.Collections;
using ChatTwo.Code; using ChatTwo.Code;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Ui; using ChatTwo.Ui;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys;
using ImGuiNET; using ImGuiNET;
namespace ChatTwo; 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] [Serializable]
internal class Configuration : IPluginConfiguration internal class Configuration : IPluginConfiguration
{ {
@@ -68,6 +89,9 @@ internal class Configuration : IPluginConfiguration
public bool OverrideStyle; public bool OverrideStyle;
public string? ChosenStyle; public string? ChosenStyle;
public ConfigKeyBind? ChatTabForward = null;
public ConfigKeyBind? ChatTabBackward = null;
internal void UpdateFrom(Configuration other, bool backToOriginal) internal void UpdateFrom(Configuration other, bool backToOriginal)
{ {
if (backToOriginal) if (backToOriginal)
@@ -123,6 +147,8 @@ internal class Configuration : IPluginConfiguration
Tabs = other.Tabs.Select(t => t.Clone()).ToList(); Tabs = other.Tabs.Select(t => t.Clone()).ToList();
OverrideStyle = other.OverrideStyle; OverrideStyle = other.OverrideStyle;
ChosenStyle = other.ChosenStyle; ChosenStyle = other.ChosenStyle;
ChatTabForward = other.ChatTabForward;
ChatTabBackward = other.ChatTabBackward;
} }
} }
+5 -216
View File
@@ -1,15 +1,11 @@
using System.Numerics; using System.Text;
using System.Text;
using ChatTwo.Code; using ChatTwo.Code;
using ChatTwo.GameFunctions.Types; using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Config;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
@@ -29,10 +25,6 @@ namespace ChatTwo.GameFunctions;
internal sealed unsafe class Chat : IDisposable internal sealed unsafe class Chat : IDisposable
{ {
// Functions // Functions
// Replace with <https://github.com/aers/FFXIVClientStructs/pull/1036>
[Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")]
private readonly delegate* unmanaged<UIInputData*, Utf8String*, nint, uint> GetKeybindNative = null!;
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")] [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")]
private readonly delegate* unmanaged<RaptureLogModule*, ushort, Utf8String*, Utf8String*, ulong, ulong, ushort, byte, int, byte, void> PrintTellNative = null!; private readonly delegate* unmanaged<RaptureLogModule*, ushort, Utf8String*, Utf8String*, ulong, ulong, ushort, byte, int, byte, void> PrintTellNative = null!;
@@ -54,11 +46,6 @@ internal sealed unsafe class Chat : IDisposable
[Signature("48 8D 35 ?? ?? ?? ?? 8B 05", ScanType = ScanType.StaticAddress)] [Signature("48 8D 35 ?? ?? ?? ?? 8B 05", ScanType = ScanType.StaticAddress)]
private readonly char* CurrentCharacter = null!; private readonly char* CurrentCharacter = null!;
// Events
internal event ChatActivatedEventDelegate? Activated;
internal delegate void ChatActivatedEventDelegate(ChatActivatedArgs args);
private Plugin Plugin { get; } private Plugin Plugin { get; }
/// <summary> /// <summary>
@@ -70,9 +57,6 @@ internal sealed unsafe class Chat : IDisposable
internal bool UsesTellTempChannel { get; set; } internal bool UsesTellTempChannel { get; set; }
internal InputChannel? PreviousChannel { get; private set; } internal InputChannel? PreviousChannel { get; private set; }
private bool DirectChat;
private long LastRefresh;
internal Chat(Plugin plugin) internal Chat(Plugin plugin)
{ {
Plugin = plugin; Plugin = plugin;
@@ -92,7 +76,6 @@ internal sealed unsafe class Chat : IDisposable
// EurekaContextMenuTellHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTargetInForay>(RaptureShellModule.MemberFunctionPointers.SetContextTellTargetInForay, SetContextTellTargetInForay); // EurekaContextMenuTellHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTargetInForay>(RaptureShellModule.MemberFunctionPointers.SetContextTellTargetInForay, SetContextTellTargetInForay);
// EurekaContextMenuTellHook.Enable(); // EurekaContextMenuTellHook.Enable();
Plugin.Framework.Update += InterceptKeybinds;
Plugin.ClientState.Login += Login; Plugin.ClientState.Login += Login;
Login(); Login();
} }
@@ -100,15 +83,12 @@ internal sealed unsafe class Chat : IDisposable
public void Dispose() public void Dispose()
{ {
Plugin.ClientState.Login -= Login; Plugin.ClientState.Login -= Login;
Plugin.Framework.Update -= InterceptKeybinds;
SetChatLogTellTargetHook?.Dispose(); SetChatLogTellTargetHook?.Dispose();
ReplyInSelectedChatModeHook?.Dispose(); ReplyInSelectedChatModeHook?.Dispose();
ChangeChannelNameHook?.Dispose(); ChangeChannelNameHook?.Dispose();
ChatLogRefreshHook?.Dispose(); ChatLogRefreshHook?.Dispose();
EurekaContextMenuTellHook?.Dispose(); EurekaContextMenuTellHook?.Dispose();
Activated = null;
} }
internal string? GetLinkshellName(uint idx) internal string? GetLinkshellName(uint idx)
@@ -164,165 +144,6 @@ internal sealed unsafe class Chat : IDisposable
return 0xFF | (rgb << 8); return 0xFF | (rgb << 8);
} }
private readonly Dictionary<string, Keybind> _keybinds = new();
internal IReadOnlyDictionary<string, Keybind> Keybinds => _keybinds;
internal static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept = new Dictionary<string, ChannelSwitchInfo>
{
["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<ModifierFlag>())
{
var modifierKey = GetKeyForModifier(modifier);
if (modifierKey != VirtualKey.NO_KEY && Plugin.KeyState[modifierKey])
modifierState |= modifier;
}
var turnedOff = new Dictionary<VirtualKey, (uint, string)>();
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() private void Login()
{ {
var agent = AgentChatLog.Instance(); 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)) if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C))
return ChatLogRefreshHook!.Original(log, eventId, value); return ChatLogRefreshHook!.Original(log, eventId, value);
if (DirectChat && CurrentCharacter != null) if (Plugin.Functions.KeybindManager.DirectChat && CurrentCharacter != null)
{ {
// FIXME: this whole system sucks // FIXME: this whole system sucks
// FIXME v2: I hate everything about this, but it works // FIXME v2: I hate everything about this, but it works
@@ -359,7 +180,7 @@ internal sealed unsafe class Chat : IDisposable
try try
{ {
Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, }); Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, });
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -380,7 +201,7 @@ internal sealed unsafe class Chat : IDisposable
try try
{ {
Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, }); Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, });
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -446,7 +267,7 @@ internal sealed unsafe class Chat : IDisposable
try try
{ {
var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason); 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, TellReason = (TellReason) reason,
TellTarget = target, TellTarget = target,
@@ -530,38 +351,6 @@ internal sealed unsafe class Chat : IDisposable
utfWorld->Dtor(true); 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) internal TellHistoryInfo? GetTellHistoryInfo(int index)
{ {
var acquaintance = AcquaintanceModule.Instance()->GetTellHistory(index); var acquaintance = AcquaintanceModule.Instance()->GetTellHistory(index);
+3
View File
@@ -25,11 +25,13 @@ internal unsafe class GameFunctions : IDisposable
#endregion #endregion
private Plugin Plugin { get; } private Plugin Plugin { get; }
internal KeybindManager KeybindManager { get; }
internal Chat Chat { get; } internal Chat Chat { get; }
internal GameFunctions(Plugin plugin) internal GameFunctions(Plugin plugin)
{ {
Plugin = plugin; Plugin = plugin;
KeybindManager = new KeybindManager(plugin);
Chat = new Chat(Plugin); Chat = new Chat(Plugin);
Plugin.GameInteropProvider.InitializeFromAttributes(this); Plugin.GameInteropProvider.InitializeFromAttributes(this);
@@ -40,6 +42,7 @@ internal unsafe class GameFunctions : IDisposable
public void Dispose() public void Dispose()
{ {
Chat.Dispose(); Chat.Dispose();
KeybindManager.Dispose();
ResolveTextCommandPlaceholderHook?.Dispose(); ResolveTextCommandPlaceholderHook?.Dispose();
+482
View File
@@ -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 <https://github.com/aers/FFXIVClientStructs/pull/1036>
[Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")]
private readonly delegate* unmanaged<UIInputData*, Utf8String*, nint, uint> GetKeybindNative = null!;
private Plugin Plugin { get; }
internal bool DirectChat;
private long LastRefresh;
private readonly Dictionary<string, Keybind> Keybinds = new();
private static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept = new Dictionary<string, ChannelSwitchInfo>
{
["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<VirtualKey> 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
};
}
}
@@ -3,6 +3,7 @@ namespace ChatTwo.GameFunctions.Types;
[Flags] [Flags]
internal enum ModifierFlag internal enum ModifierFlag
{ {
None = 0,
Shift = 1 << 0, Shift = 1 << 0,
Ctrl = 1 << 1, Ctrl = 1 << 1,
Alt = 1 << 2, Alt = 1 << 2,
+63
View File
@@ -1715,6 +1715,51 @@ namespace ChatTwo.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to ESC to clear.
/// </summary>
internal static string Keybind_EscToClear {
get {
return ResourceManager.GetString("Keybind_EscToClear", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Alt.
/// </summary>
internal static string Keybind_Modifier_Alt {
get {
return ResourceManager.GetString("Keybind_Modifier_Alt", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ctrl.
/// </summary>
internal static string Keybind_Modifier_Ctrl {
get {
return ResourceManager.GetString("Keybind_Modifier_Ctrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shift.
/// </summary>
internal static string Keybind_Modifier_Shift {
get {
return ResourceManager.GetString("Keybind_Modifier_Shift", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to none set.
/// </summary>
internal static string Keybind_None {
get {
return ResourceManager.GetString("Keybind_None", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Flexible. /// Looks up a localized string similar to Flexible.
/// </summary> /// </summary>
@@ -1940,6 +1985,24 @@ namespace ChatTwo.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Cycle chat tab backwards keybind.
/// </summary>
internal static string Options_ChatTabBackwardKeybind_Name {
get {
return ResourceManager.GetString("Options_ChatTabBackwardKeybind_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cycle chat tab forwards keybind.
/// </summary>
internal static string Options_ChatTabForwardKeybind_Name {
get {
return ResourceManager.GetString("Options_ChatTabForwardKeybind_Name", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Clear the message history database. /// Looks up a localized string similar to Clear the message history database.
/// </summary> /// </summary>
+21
View File
@@ -514,6 +514,27 @@
<data name="Options_OverrideStyleDropdown_Name"> <data name="Options_OverrideStyleDropdown_Name">
<value>Styles</value> <value>Styles</value>
</data> </data>
<data name="Options_ChatTabForwardKeybind_Name">
<value>Cycle chat tab forwards keybind</value>
</data>
<data name="Options_ChatTabBackwardKeybind_Name">
<value>Cycle chat tab backwards keybind</value>
</data>
<data name="Keybind_None">
<value>none set</value>
</data>
<data name="Keybind_EscToClear">
<value>ESC to clear</value>
</data>
<data name="Keybind_Modifier_Ctrl">
<value>Ctrl</value>
</data>
<data name="Keybind_Modifier_Alt">
<value>Alt</value>
</data>
<data name="Keybind_Modifier_Shift">
<value>Shift</value>
</data>
<data name="AutoTranslate_Completion_Key"> <data name="AutoTranslate_Completion_Key">
<value>Ctrl + {0}</value> <value>Ctrl + {0}</value>
</data> </data>
+20 -64
View File
@@ -3,12 +3,12 @@ using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using ChatTwo.Code; using ChatTwo.Code;
using ChatTwo.GameFunctions;
using ChatTwo.GameFunctions.Types; using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface; using Dalamud.Interface;
@@ -41,6 +41,7 @@ public sealed class ChatLogWindow : Window
internal Vector4 DefaultText { get; set; } internal Vector4 DefaultText { get; set; }
internal int? WantedTab { get; set; }
internal Tab? CurrentTab internal Tab? CurrentTab
{ {
get get
@@ -120,7 +121,6 @@ public sealed class ChatLogWindow : Window
TextCommandSheet = Plugin.DataManager.GetExcelSheet<TextCommand>()!; TextCommandSheet = Plugin.DataManager.GetExcelSheet<TextCommand>()!;
FontIcon = Plugin.TextureProvider.CreateFromTexFile(Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!); FontIcon = Plugin.TextureProvider.CreateFromTexFile(Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!);
Plugin.Functions.Chat.Activated += Activated;
Plugin.ClientState.Login += Login; Plugin.ClientState.Login += Login;
Plugin.ClientState.Logout += Logout; Plugin.ClientState.Logout += Logout;
@@ -132,7 +132,6 @@ public sealed class ChatLogWindow : Window
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip); Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip);
Plugin.ClientState.Logout -= Logout; Plugin.ClientState.Logout -= Logout;
Plugin.ClientState.Login -= Login; Plugin.ClientState.Login -= Login;
Plugin.Functions.Chat.Activated -= Activated;
FontIcon?.Dispose(); FontIcon?.Dispose();
Plugin.Commands.Register("/chat2").Execute -= ToggleChat; Plugin.Commands.Register("/chat2").Execute -= ToggleChat;
Plugin.Commands.Register("/clearlog2").Execute -= ClearLog; Plugin.Commands.Register("/clearlog2").Execute -= ClearLog;
@@ -148,7 +147,7 @@ public sealed class ChatLogWindow : Window
Plugin.MessageManager.FilterAllTabsAsync(); Plugin.MessageManager.FilterAllTabsAsync();
} }
private void Activated(ChatActivatedArgs args) internal void Activated(ChatActivatedArgs args)
{ {
Activate = true; Activate = true;
PlayedClosingSound = false; PlayedClosingSound = false;
@@ -336,63 +335,14 @@ public sealed class ChatLogWindow : Window
return height; return height;
} }
private void HandleKeybinds(bool modifiersOnly = false) internal void ChangeTab(int index) => WantedTab = index;
internal void ChangeTabDelta(int offset)
{ {
var modifierState = (ModifierFlag) 0; var newIndex = (LastTab + offset) % Plugin.Config.Tabs.Count;
if (ImGui.GetIO().KeyAlt) while (newIndex < 0)
modifierState |= ModifierFlag.Alt; newIndex += Plugin.Config.Tabs.Count;
ChangeTab(newIndex);
if (ImGui.GetIO().KeyCtrl)
modifierState |= ModifierFlag.Ctrl;
if (ImGui.GetIO().KeyShift)
modifierState |= ModifierFlag.Shift;
var turnedOff = new Dictionary<VirtualKey, (uint, string)>();
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");
}
}
} }
private void TabChannelSwitch(Tab tab) 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()) if (ImGui.IsItemActive())
{ {
HandleKeybinds(true); Plugin.Functions.KeybindManager.HandleKeybinds(KeyboardSource.ImGui, true, true);
LastActivityTime = FrameTime; LastActivityTime = FrameTime;
} }
@@ -1158,7 +1109,10 @@ public sealed class ChatLogWindow : Window
continue; continue;
var unread = tabI == LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; 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); DrawTabContextMenu(tab, tabI);
if (!tabItem.Success) if (!tabItem.Success)
@@ -1174,6 +1128,7 @@ public sealed class ChatLogWindow : Window
DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), switchedTab); DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), switchedTab);
} }
WantedTab = null;
return currentTab; return currentTab;
} }
@@ -1203,10 +1158,10 @@ public sealed class ChatLogWindow : Window
continue; continue;
var unread = tabI == LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; 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); DrawTabContextMenu(tab, tabI);
if (!clicked) if (!clicked && WantedTab != tabI)
continue; continue;
currentTab = tabI; currentTab = tabI;
@@ -1229,6 +1184,7 @@ public sealed class ChatLogWindow : Window
if (currentTab > -1) if (currentTab > -1)
DrawMessageLog(Plugin.Config.Tabs[currentTab], PayloadHandler, childHeight, switchedTab); DrawMessageLog(Plugin.Config.Tabs[currentTab], PayloadHandler, childHeight, switchedTab);
WantedTab = null;
return currentTab; return currentTab;
} }
+12
View File
@@ -72,6 +72,18 @@ internal sealed class ChatLog : ISettingsTab
ImGui.Separator(); ImGui.Separator();
ImGui.Spacing(); 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); ImGui.TextUnformatted(Language.Options_AdjustPosition_Name);
var pos = Plugin.ChatLogWindow.LastWindowPos; var pos = Plugin.ChatLogWindow.LastWindowPos;
ImGui.SetNextItemWidth(-1); ImGui.SetNextItemWidth(-1);
+70
View File
@@ -1,5 +1,7 @@
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface; 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<VirtualKey>())
{
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) public static void DrawArrows(ref int selected, int min, int max, float spacing, int id = 0)
{ {
// Prevents changing values from triggering EndDisable // Prevents changing values from triggering EndDisable