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? _listeningLastKeys; private HashSet? _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(); 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(); var currentPads = new HashSet(); 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(currentKeys); _listeningLastPads = new HashSet(currentPads); return; } var modifierKeySet = new HashSet(ModifierKeys); var modifierPadSet = new HashSet(ModifierButtons); var heldModifierKeys = new HashSet(currentKeys.Where(k => modifierKeySet.Contains(k) && _listeningLastKeys.Contains(k))); var heldModifierPads = new HashSet(currentPads.Where(b => modifierPadSet.Contains(b) && _listeningLastPads!.Contains(b))); string? triggerPart = null; foreach (var vk in Plugin.KeyState?.GetValidVirtualKeys() ?? Array.Empty()) { 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(currentKeys); _listeningLastPads = new HashSet(currentPads); if (triggerPart == null) return; var parts = new List(); 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 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(); 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); } } }