f3e10f27d2
- 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
246 lines
10 KiB
C#
246 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Dalamud.Game.ClientState.GamePad;
|
|
using Dalamud.Game.ClientState.Keys;
|
|
using Dalamud.Interface.Windowing;
|
|
using HSUI.Config;
|
|
using HSUI.Interface.GeneralElements;
|
|
using Dalamud.Bindings.ImGui;
|
|
|
|
namespace HSUI.Config.Windows
|
|
{
|
|
public class ControllerBarKeybindsWindow : Window
|
|
{
|
|
private ControllerBarKeybindsConfig? _config;
|
|
private int _selectedBar = 1;
|
|
private int _listeningBar = -1;
|
|
private int _listeningSlot = -1;
|
|
private HashSet<VirtualKey>? _listeningLastKeys;
|
|
private HashSet<GamepadButtons>? _listeningLastPads;
|
|
|
|
private const int SlotsPerBar = 8;
|
|
private static readonly GamepadButtons[] ModifierButtons = { GamepadButtons.L1, GamepadButtons.L2, GamepadButtons.R1, GamepadButtons.R2 };
|
|
private static readonly VirtualKey[] ModifierKeys = { VirtualKey.LSHIFT, VirtualKey.RSHIFT, VirtualKey.LCONTROL, VirtualKey.RCONTROL, VirtualKey.LMENU, VirtualKey.RMENU };
|
|
private const float CellSize = 44;
|
|
private const float Pad = 4;
|
|
private const float Gap = 8;
|
|
|
|
public ControllerBarKeybindsWindow() : base("Controller bar keybinds")
|
|
{
|
|
Size = new Vector2(520, 380);
|
|
SizeCondition = ImGuiCond.FirstUseEver;
|
|
Flags = ImGuiWindowFlags.NoScrollbar;
|
|
}
|
|
|
|
public override void Draw()
|
|
{
|
|
_config = ConfigurationManager.Instance?.GetConfigObject<ControllerBarKeybindsConfig>();
|
|
if (_config == null) return;
|
|
|
|
ImGui.Text("Assign keyboard or gamepad buttons to controller cross bar slots.");
|
|
ImGui.Spacing();
|
|
|
|
// Bar selector
|
|
ImGui.Text("Cross bar:");
|
|
ImGui.SameLine(100);
|
|
for (int b = 1; b <= 8; b++)
|
|
{
|
|
if (b > 1) ImGui.SameLine();
|
|
if (ImGui.Button($"{(b == _selectedBar ? "[" : " ")}{b}{(b == _selectedBar ? "]" : " ")}", new Vector2(32, 0)))
|
|
_selectedBar = b;
|
|
}
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
if (_listeningBar >= 0 && _listeningSlot >= 0)
|
|
{
|
|
ImGui.TextColored(new Vector4(1, 0.8f, 0, 1), "Hold trigger (L2/R2/L1/R1) or modifier (Shift/Ctrl/Alt), then press a key or button. Or press a key/button alone.");
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("Cancel"))
|
|
{
|
|
_listeningBar = -1;
|
|
_listeningSlot = -1;
|
|
_listeningLastKeys = null;
|
|
_listeningLastPads = null;
|
|
}
|
|
PollAndAssign();
|
|
}
|
|
else
|
|
{
|
|
ImGui.Text($"Bar {_selectedBar} — click a slot, then hold modifiers (e.g. L2) and press a button. Right-click to clear.");
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// Two crosses side by side (same layout as CrossBarHud)
|
|
float leftCrossW = 3 * (CellSize + Pad);
|
|
float startX = ImGui.GetCursorScreenPos().X;
|
|
float startY = ImGui.GetCursorScreenPos().Y;
|
|
|
|
for (int slot = 0; slot < SlotsPerBar; slot++)
|
|
{
|
|
(int col, int row) = GetCrossSlotLayout(slot);
|
|
float x = startX + (slot < 4 ? col * (CellSize + Pad) : leftCrossW + Gap + col * (CellSize + Pad));
|
|
float y = startY + row * (CellSize + Pad);
|
|
|
|
ImGui.SetCursorScreenPos(new Vector2(x, y));
|
|
string current = _config.GetKeybind(_selectedBar, slot);
|
|
string label = string.IsNullOrEmpty(current) ? "None" : GetKeybindDisplayName(current);
|
|
bool listening = _listeningBar == _selectedBar && _listeningSlot == slot;
|
|
|
|
if (listening)
|
|
ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0.4f, 0.6f, 0.2f, 0.8f));
|
|
if (ImGui.Button($"##bar{_selectedBar}_slot{slot}", new Vector2(CellSize, CellSize)))
|
|
{
|
|
_listeningBar = _selectedBar;
|
|
_listeningSlot = slot;
|
|
}
|
|
if (listening) ImGui.PopStyleColor();
|
|
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
if (ImGui.IsMouseClicked(ImGuiMouseButton.Right))
|
|
{
|
|
_config.SetKeybind(_selectedBar, slot, "");
|
|
ConfigurationManager.Instance?.SaveConfigurations();
|
|
}
|
|
}
|
|
|
|
string shortLabel = label.Length > 6 ? label.Substring(0, 6) + "…" : label;
|
|
Vector2 textSize = ImGui.CalcTextSize(shortLabel);
|
|
ImGui.SetCursorScreenPos(new Vector2(x + (CellSize - textSize.X) * 0.5f, y + (CellSize - textSize.Y) * 0.5f));
|
|
ImGui.Text(shortLabel);
|
|
}
|
|
|
|
ImGui.SetCursorScreenPos(new Vector2(startX, startY + 3 * (CellSize + Pad) + 20));
|
|
ImGui.Spacing();
|
|
if (ImGui.Button("Close"))
|
|
IsOpen = false;
|
|
}
|
|
|
|
private static (int col, int row) GetCrossSlotLayout(int slotIndex)
|
|
{
|
|
if (slotIndex < 4)
|
|
return slotIndex switch { 0 => (1, 0), 1 => (0, 1), 2 => (2, 1), _ => (1, 2) };
|
|
int i = slotIndex - 4;
|
|
return i switch { 0 => (1, 0), 1 => (0, 1), 2 => (2, 1), _ => (1, 2) };
|
|
}
|
|
|
|
private void PollAndAssign()
|
|
{
|
|
var currentKeys = new HashSet<VirtualKey>();
|
|
var currentPads = new HashSet<GamepadButtons>();
|
|
|
|
if (Plugin.KeyState != null)
|
|
{
|
|
foreach (var vk in Plugin.KeyState.GetValidVirtualKeys())
|
|
{
|
|
if (vk == VirtualKey.ESCAPE) continue;
|
|
if (Plugin.KeyState[vk])
|
|
currentKeys.Add(vk);
|
|
}
|
|
}
|
|
if (Plugin.GamepadState != null)
|
|
{
|
|
foreach (var btn in GetGamepadButtonsToPoll())
|
|
{
|
|
if (Plugin.GamepadState.Raw(btn) > 0.5f)
|
|
currentPads.Add(btn);
|
|
}
|
|
}
|
|
|
|
// First frame of listening: record state as "last" and wait for next frame
|
|
if (_listeningLastKeys == null)
|
|
{
|
|
_listeningLastKeys = new HashSet<VirtualKey>(currentKeys);
|
|
_listeningLastPads = new HashSet<GamepadButtons>(currentPads);
|
|
return;
|
|
}
|
|
|
|
var modifierKeySet = new HashSet<VirtualKey>(ModifierKeys);
|
|
var modifierPadSet = new HashSet<GamepadButtons>(ModifierButtons);
|
|
|
|
var heldModifierKeys = new HashSet<VirtualKey>(currentKeys.Where(k => modifierKeySet.Contains(k) && _listeningLastKeys.Contains(k)));
|
|
var heldModifierPads = new HashSet<GamepadButtons>(currentPads.Where(b => modifierPadSet.Contains(b) && _listeningLastPads!.Contains(b)));
|
|
|
|
string? triggerPart = null;
|
|
foreach (var vk in Plugin.KeyState?.GetValidVirtualKeys() ?? Array.Empty<VirtualKey>())
|
|
{
|
|
if (vk == VirtualKey.ESCAPE) continue;
|
|
if (currentKeys.Contains(vk) && !_listeningLastKeys.Contains(vk))
|
|
{
|
|
triggerPart = "Key:" + vk;
|
|
break;
|
|
}
|
|
}
|
|
if (triggerPart == null && Plugin.GamepadState != null)
|
|
{
|
|
foreach (var btn in GetGamepadButtonsToPoll())
|
|
{
|
|
if (currentPads.Contains(btn) && !_listeningLastPads!.Contains(btn))
|
|
{
|
|
triggerPart = "Pad:" + btn;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_listeningLastKeys = new HashSet<VirtualKey>(currentKeys);
|
|
_listeningLastPads = new HashSet<GamepadButtons>(currentPads);
|
|
|
|
if (triggerPart == null) return;
|
|
|
|
var parts = new List<string>();
|
|
foreach (var vk in heldModifierKeys.OrderBy(k => k.ToString()))
|
|
parts.Add("Key:" + vk);
|
|
foreach (var btn in heldModifierPads.OrderBy(b => b.ToString()))
|
|
parts.Add("Pad:" + btn);
|
|
parts.Add(triggerPart);
|
|
|
|
string assigned = string.Join("+", parts);
|
|
if (_listeningBar >= 0 && _listeningSlot >= 0 && _config != null)
|
|
{
|
|
_config.SetKeybind(_listeningBar, _listeningSlot, assigned);
|
|
ConfigurationManager.Instance?.SaveConfigurations();
|
|
_listeningBar = -1;
|
|
_listeningSlot = -1;
|
|
_listeningLastKeys = null;
|
|
_listeningLastPads = null;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<GamepadButtons> GetGamepadButtonsToPoll()
|
|
{
|
|
return new[]
|
|
{
|
|
GamepadButtons.DpadUp, GamepadButtons.DpadDown, GamepadButtons.DpadLeft, GamepadButtons.DpadRight,
|
|
GamepadButtons.North, GamepadButtons.South, GamepadButtons.West, GamepadButtons.East,
|
|
GamepadButtons.L1, GamepadButtons.L2, GamepadButtons.L3,
|
|
GamepadButtons.R1, GamepadButtons.R2, GamepadButtons.R3,
|
|
GamepadButtons.Start, GamepadButtons.Select
|
|
};
|
|
}
|
|
|
|
private static string GetKeybindDisplayName(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value)) return "None";
|
|
var parts = value.Split('+');
|
|
var display = new List<string>();
|
|
foreach (var p in parts)
|
|
{
|
|
string s = p.Trim();
|
|
if (s.StartsWith("Key:", StringComparison.OrdinalIgnoreCase))
|
|
display.Add(s.Length > 4 ? s.Substring(4).Replace("VK_", "") : s);
|
|
else if (s.StartsWith("Pad:", StringComparison.OrdinalIgnoreCase))
|
|
display.Add(s.Length > 4 ? s.Substring(4) : s);
|
|
else
|
|
display.Add(s);
|
|
}
|
|
return string.Join(" + ", display);
|
|
}
|
|
}
|
|
}
|