Merge pull request #82 from deansheather/dean/cycle-tabs-hotkeys
feat: add configurable hotkeys to cycle tabs
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <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")]
|
||||
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)]
|
||||
private readonly char* CurrentCharacter = null!;
|
||||
|
||||
// Events
|
||||
|
||||
internal event ChatActivatedEventDelegate? Activated;
|
||||
internal delegate void ChatActivatedEventDelegate(ChatActivatedArgs args);
|
||||
|
||||
private Plugin Plugin { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -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.Delegates.SetContextTellTargetInForay>(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<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()
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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]
|
||||
internal enum ModifierFlag
|
||||
{
|
||||
None = 0,
|
||||
Shift = 1 << 0,
|
||||
Ctrl = 1 << 1,
|
||||
Alt = 1 << 2,
|
||||
|
||||
Generated
+63
@@ -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>
|
||||
/// Looks up a localized string similar to Flexible.
|
||||
/// </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>
|
||||
/// Looks up a localized string similar to Clear the message history database.
|
||||
/// </summary>
|
||||
|
||||
@@ -514,6 +514,27 @@
|
||||
<data name="Options_OverrideStyleDropdown_Name">
|
||||
<value>Styles</value>
|
||||
</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">
|
||||
<value>Ctrl + {0}</value>
|
||||
</data>
|
||||
|
||||
+20
-64
@@ -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<TextCommand>()!;
|
||||
FontIcon = Plugin.TextureProvider.CreateFromTexFile(Plugin.DataManager.GetFile<TexFile>("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<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");
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<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)
|
||||
{
|
||||
// Prevents changing values from triggering EndDisable
|
||||
|
||||
Reference in New Issue
Block a user