feat: Controller hotbars with cross layout, separate storage, and sync with game

- Add controller hotbars: 8 cross bars (L2/R2 style), separate from normal hotbars 1-8
- Controller bar slot data stored in config (not game StandardHotbars) so layouts can differ per mode
- Drag-and-drop on controller bars: from game, shift+drag rearrange, release outside to clear
- Independent controller bar keybinds with modifier+trigger combinations (e.g. L2+South)
- Optional 'Sync bar mode with game client': follow Character Config Mouse/Gamepad toggle (PadMode)
- Clone/copy actions: normal hotbars ↔ controller bars
- Restore controller bar layout button; deploy to devPlugins on Release build

Made-with: Cursor
This commit is contained in:
Jorg
2026-02-26 22:18:40 -06:00
parent 369a770162
commit f3e10f27d2
13 changed files with 1706 additions and 21 deletions
+106
View File
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.GamePad;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Plugin.Services;
using HSUI.Config;
using HSUI.Interface.GeneralElements;
namespace HSUI.Helpers
{
/// <summary>Polls controller bar keybinds and executes slots when keys/buttons are pressed. Supports combinations (e.g. L2+South).</summary>
public static class ControllerBarKeybindExecutor
{
private static readonly Dictionary<string, bool> _triggerWasDown = new Dictionary<string, bool>();
public static void Process(IFramework framework)
{
try
{
if (!ControllerHotbarsConfig.GetEffectiveControllerHotbarsEnabled())
return;
var keybindsConfig = ConfigurationManager.Instance?.GetConfigObject<ControllerBarKeybindsConfig>();
if (keybindsConfig == null)
return;
if (ActionBarsManager.Instance == null)
return;
for (int bar = 1; bar <= ControllerBarKeybindsConfig.Bars; bar++)
{
for (int slot = 0; slot < ControllerBarKeybindsConfig.SlotsPerBar; slot++)
{
string binding = keybindsConfig.GetKeybind(bar, slot);
if (string.IsNullOrEmpty(binding)) continue;
bool triggered = IsKeybindTriggered(binding);
if (triggered)
ActionBarsManager.Instance.ExecuteControllerSlot(bar, slot);
}
}
}
catch (Exception ex)
{
Plugin.Logger?.Warning($"[HSUI ControllerBarKeybinds] Process: {ex.Message}");
}
}
/// <summary>True when all modifiers are held and the trigger key/button was just pressed (edge).</summary>
private static bool IsKeybindTriggered(string binding)
{
if (string.IsNullOrEmpty(binding)) return false;
string[] parts = binding.Split('+');
for (int i = 0; i < parts.Length; i++)
parts[i] = parts[i].Trim();
if (parts.Length == 0) return false;
if (parts.Length == 1)
{
bool down = IsSingleKeybindDown(parts[0]);
if (!_triggerWasDown.TryGetValue(binding, out bool wasDown)) wasDown = false;
_triggerWasDown[binding] = down;
return down && !wasDown;
}
string triggerPart = parts[parts.Length - 1];
for (int i = 0; i < parts.Length - 1; i++)
{
if (!IsSingleKeybindDown(parts[i]))
return false;
}
bool triggerDown = IsSingleKeybindDown(triggerPart);
if (!_triggerWasDown.TryGetValue(binding, out bool triggerWasDown))
triggerWasDown = false;
_triggerWasDown[binding] = triggerDown;
return triggerDown && !triggerWasDown;
}
private static bool IsSingleKeybindDown(string part)
{
if (string.IsNullOrEmpty(part)) return false;
if (part.StartsWith("Key:", StringComparison.OrdinalIgnoreCase))
{
string vkStr = part.Substring(4).Trim();
if (Plugin.KeyState == null) return false;
if (Enum.TryParse<VirtualKey>(vkStr, true, out var vk)
&& Plugin.KeyState.IsVirtualKeyValid((int)vk))
return Plugin.KeyState[vk];
return false;
}
if (part.StartsWith("Pad:", StringComparison.OrdinalIgnoreCase))
{
string padStr = part.Substring(4).Trim();
if (Plugin.GamepadState == null) return false;
if (Enum.TryParse<GamepadButtons>(padStr, true, out var btn))
return Plugin.GamepadState.Raw(btn) > 0.5f;
return false;
}
return false;
}
}
}