#nullable enable using System; using System.Collections.Generic; using Content.Client.UserInterface.Stylesheets; using Content.Shared.Input; using Robust.Client.Input; using Robust.Client.Interfaces.Input; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Client.UserInterface { public sealed partial class OptionsMenu { private sealed class KeyRebindControl : Control { // List of key functions that must be registered as toggle instead. private static readonly HashSet ToggleFunctions = new() { EngineKeyFunctions.ShowDebugMonitors, EngineKeyFunctions.HideUI, }; [Dependency] private readonly IInputManager _inputManager = default!; private BindButton? _currentlyRebinding; private readonly Dictionary _keyControls = new(); private readonly List _deferCommands = new(); public KeyRebindControl() { IoCManager.InjectDependencies(this); Button resetAllButton; var vBox = new VBoxContainer(); AddChild(new VBoxContainer { Children = { new ScrollContainer { SizeFlagsVertical = SizeFlags.FillExpand, Children = { new MarginContainer { MarginLeftOverride = 2, Children = { vBox } } } }, new StripeBack { HasBottomEdge = false, HasMargins = false, Children = { new HBoxContainer { Children = { new Control {CustomMinimumSize = (2, 0)}, new Label { StyleClasses = {StyleBase.StyleClassLabelSubText}, Text = "Click to change binding, right-click to clear" }, (resetAllButton = new Button { Text = "Reset ALL keybinds", StyleClasses = {StyleBase.ButtonCaution}, SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand }) } } } } } }); resetAllButton.OnPressed += args => { _deferCommands.Add(() => { _inputManager.ResetAllBindings(); _inputManager.SaveToUserData(); }); }; var first = true; void AddHeader(string headerContents) { if (!first) { vBox.AddChild(new Control {CustomMinimumSize = (0, 8)}); } first = false; vBox.AddChild(new Label { Text = headerContents, FontColorOverride = StyleNano.NanoGold, StyleClasses = {StyleNano.StyleClassLabelKeyText} }); } void AddButton(BoundKeyFunction function, string name) { var control = new KeyControl(this, name, function); vBox.AddChild(control); _keyControls.Add(function, control); } AddHeader("Movement"); AddButton(EngineKeyFunctions.MoveUp, "Move up"); AddButton(EngineKeyFunctions.MoveLeft, "Move left"); AddButton(EngineKeyFunctions.MoveDown, "Move down"); AddButton(EngineKeyFunctions.MoveRight, "Move right"); AddButton(EngineKeyFunctions.Walk, "Walk"); AddHeader("Basic Interaction"); AddButton(EngineKeyFunctions.Use, "Use"); AddButton(ContentKeyFunctions.WideAttack, "Wide attack"); AddButton(ContentKeyFunctions.ActivateItemInHand, "Activate item in hand"); AddButton(ContentKeyFunctions.ActivateItemInWorld, "Activate item in world"); AddButton(ContentKeyFunctions.Drop, "Drop item"); AddButton(ContentKeyFunctions.ExamineEntity, "Examine"); AddButton(ContentKeyFunctions.SwapHands, "Swap hands"); AddButton(ContentKeyFunctions.ToggleCombatMode, "Toggle combat mode"); AddHeader("Advanced Interaction"); AddButton(ContentKeyFunctions.SmartEquipBackpack, "Smart-equip to backpack"); AddButton(ContentKeyFunctions.SmartEquipBelt, "Smart-equip to belt"); AddButton(ContentKeyFunctions.ThrowItemInHand, "Throw item"); AddButton(ContentKeyFunctions.TryPullObject, "Pull object"); AddButton(ContentKeyFunctions.MovePulledObject, "Move pulled object"); AddButton(ContentKeyFunctions.ReleasePulledObject, "Release pulled object"); AddButton(ContentKeyFunctions.Point, "Point at location"); AddHeader("User Interface"); AddButton(ContentKeyFunctions.FocusChat, "Focus chat"); AddButton(ContentKeyFunctions.FocusOOC, "Focus chat (OOC)"); AddButton(ContentKeyFunctions.FocusAdminChat, "Focus chat (admin)"); AddButton(ContentKeyFunctions.OpenCharacterMenu, "Open character menu"); AddButton(ContentKeyFunctions.OpenContextMenu, "Open context menu"); AddButton(ContentKeyFunctions.OpenCraftingMenu, "Open crafting menu"); AddButton(ContentKeyFunctions.OpenInventoryMenu, "Open inventory"); AddButton(ContentKeyFunctions.OpenTutorial, "Open tutorial"); AddButton(ContentKeyFunctions.OpenActionsMenu, "Open action menu"); AddButton(ContentKeyFunctions.OpenEntitySpawnWindow, "Open entity spawn menu"); AddButton(ContentKeyFunctions.OpenSandboxWindow, "Open sandbox menu"); AddButton(ContentKeyFunctions.OpenTileSpawnWindow, "Open tile spawn menu"); AddButton(ContentKeyFunctions.OpenAdminMenu, "Open admin menu"); AddHeader("Miscellaneous"); AddButton(ContentKeyFunctions.TakeScreenshot, "Take screenshot"); AddButton(ContentKeyFunctions.TakeScreenshotNoUI, "Take screenshot (without UI)"); AddHeader("Hotbar"); AddButton(ContentKeyFunctions.Hotbar1, "Hotbar slot 1"); AddButton(ContentKeyFunctions.Hotbar2, "Hotbar slot 2"); AddButton(ContentKeyFunctions.Hotbar3, "Hotbar slot 3"); AddButton(ContentKeyFunctions.Hotbar4, "Hotbar slot 4"); AddButton(ContentKeyFunctions.Hotbar5, "Hotbar slot 5"); AddButton(ContentKeyFunctions.Hotbar6, "Hotbar slot 6"); AddButton(ContentKeyFunctions.Hotbar7, "Hotbar slot 7"); AddButton(ContentKeyFunctions.Hotbar8, "Hotbar slot 8"); AddButton(ContentKeyFunctions.Hotbar9, "Hotbar slot 9"); AddButton(ContentKeyFunctions.Hotbar0, "Hotbar slot 0"); AddButton(ContentKeyFunctions.Loadout1, "Hotbar Loadout 1"); AddButton(ContentKeyFunctions.Loadout2, "Hotbar Loadout 2"); AddButton(ContentKeyFunctions.Loadout3, "Hotbar Loadout 3"); AddButton(ContentKeyFunctions.Loadout4, "Hotbar Loadout 4"); AddButton(ContentKeyFunctions.Loadout5, "Hotbar Loadout 5"); AddButton(ContentKeyFunctions.Loadout6, "Hotbar Loadout 6"); AddButton(ContentKeyFunctions.Loadout7, "Hotbar Loadout 7"); AddButton(ContentKeyFunctions.Loadout8, "Hotbar Loadout 8"); AddButton(ContentKeyFunctions.Loadout9, "Hotbar Loadout 9"); AddHeader("Map Editor"); AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object"); AddButton(EngineKeyFunctions.EditorCancelPlace, "Cancel placement"); AddButton(EngineKeyFunctions.EditorGridPlace, "Place in grid"); AddButton(EngineKeyFunctions.EditorLinePlace, "Place line"); AddButton(EngineKeyFunctions.EditorRotateObject, "Rotate"); AddHeader("Development"); AddButton(EngineKeyFunctions.ShowDebugConsole, "Open Console"); AddButton(EngineKeyFunctions.ShowDebugMonitors, "Show Debug Monitors"); AddButton(EngineKeyFunctions.HideUI, "Hide UI"); foreach (var control in _keyControls.Values) { UpdateKeyControl(control); } } private void UpdateKeyControl(KeyControl control) { var activeBinds = _inputManager.GetKeyBindings(control.Function); IKeyBinding? bind1 = null; IKeyBinding? bind2 = null; if (activeBinds.Count > 0) { bind1 = activeBinds[0]; if (activeBinds.Count > 1) { bind2 = activeBinds[1]; } } control.BindButton1.Binding = bind1; control.BindButton1.UpdateText(); control.BindButton2.Binding = bind2; control.BindButton2.UpdateText(); control.BindButton2.Button.Disabled = activeBinds.Count == 0; control.ResetButton.Disabled = !_inputManager.IsKeyFunctionModified(control.Function); } protected override void EnteredTree() { base.EnteredTree(); _inputManager.FirstChanceOnKeyEvent += InputManagerOnFirstChanceOnKeyEvent; _inputManager.OnKeyBindingAdded += OnKeyBindAdded; _inputManager.OnKeyBindingRemoved += OnKeyBindRemoved; } protected override void ExitedTree() { base.ExitedTree(); _inputManager.FirstChanceOnKeyEvent -= InputManagerOnFirstChanceOnKeyEvent; _inputManager.OnKeyBindingAdded -= OnKeyBindAdded; _inputManager.OnKeyBindingRemoved -= OnKeyBindRemoved; } private void OnKeyBindRemoved(IKeyBinding obj) { OnKeyBindModified(obj, true); } private void OnKeyBindAdded(IKeyBinding obj) { OnKeyBindModified(obj, false); } private void OnKeyBindModified(IKeyBinding bind, bool removal) { if (!_keyControls.TryGetValue(bind.Function, out var keyControl)) { return; } if (removal && _currentlyRebinding?.KeyControl == keyControl) { // Don't do update if the removal was from initiating a rebind. return; } UpdateKeyControl(keyControl); if (_currentlyRebinding == keyControl.BindButton1 || _currentlyRebinding == keyControl.BindButton2) { _currentlyRebinding = null; } } private void InputManagerOnFirstChanceOnKeyEvent(KeyEventArgs keyEvent, KeyEventType type) { DebugTools.Assert(IsInsideTree); if (_currentlyRebinding == null) { return; } keyEvent.Handle(); if (type != KeyEventType.Up) { return; } var key = keyEvent.Key; // Figure out modifiers based on key event. // TODO: this won't allow for combinations with keys other than the standard modifier keys, // even though the input system totally supports it. var mods = new Keyboard.Key[3]; var i = 0; if (keyEvent.Control && key != Keyboard.Key.Control) { mods[i] = Keyboard.Key.Control; i += 1; } if (keyEvent.Shift && key != Keyboard.Key.Shift) { mods[i] = Keyboard.Key.Shift; i += 1; } if (keyEvent.Alt && key != Keyboard.Key.Alt) { mods[i] = Keyboard.Key.Alt; i += 1; } // The input system can only handle 3 modifier keys so if you hold all 4 of the modifier keys // then system gets the shaft, I guess. if (keyEvent.System && i != 3 && key != Keyboard.Key.LSystem && key != Keyboard.Key.RSystem) { mods[i] = Keyboard.Key.LSystem; } var function = _currentlyRebinding.KeyControl.Function; var bindType = KeyBindingType.State; if (ToggleFunctions.Contains(function)) { bindType = KeyBindingType.Toggle; } var registration = new KeyBindingRegistration { Function = function, BaseKey = key, Mod1 = mods[0], Mod2 = mods[1], Mod3 = mods[2], Priority = 0, Type = bindType, CanFocus = key == Keyboard.Key.MouseLeft || key == Keyboard.Key.MouseRight || key == Keyboard.Key.MouseMiddle, CanRepeat = false }; _inputManager.RegisterBinding(registration); // OnKeyBindModified will cause _currentlyRebinding to be reset and the UI to update. _inputManager.SaveToUserData(); } private void RebindButtonPressed(BindButton button) { if (_currentlyRebinding != null) { return; } _currentlyRebinding = button; _currentlyRebinding.Button.Text = Loc.GetString("Press a key..."); if (button.Binding != null) { _deferCommands.Add(() => { // Have to do defer this or else there will be an exception in InputManager. // Because this IS fired from an input event. _inputManager.RemoveBinding(button.Binding); }); } } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); if (_deferCommands.Count == 0) { return; } foreach (var command in _deferCommands) { command(); } _deferCommands.Clear(); } private sealed class KeyControl : Control { public readonly BoundKeyFunction Function; public readonly BindButton BindButton1; public readonly BindButton BindButton2; public readonly Button ResetButton; public KeyControl(KeyRebindControl parent, string niceName, BoundKeyFunction function) { Function = function; var name = new Label { Text = Loc.GetString(niceName), SizeFlagsHorizontal = SizeFlags.Expand }; BindButton1 = new BindButton(parent, this, StyleBase.ButtonOpenRight); BindButton2 = new BindButton(parent, this, StyleBase.ButtonOpenLeft); ResetButton = new Button {Text = "Reset", StyleClasses = {StyleBase.ButtonCaution}}; var hBox = new HBoxContainer { Children = { new Control {CustomMinimumSize = (5, 0)}, name, BindButton1, BindButton2, new Control {CustomMinimumSize = (10, 0)}, ResetButton } }; ResetButton.OnPressed += args => { parent._deferCommands.Add(() => { parent._inputManager.ResetBindingsFor(function); parent._inputManager.SaveToUserData(); }); }; AddChild(hBox); } } private sealed class BindButton : Control { private readonly KeyRebindControl _control; public readonly KeyControl KeyControl; public readonly Button Button; public IKeyBinding? Binding; public BindButton(KeyRebindControl control, KeyControl keyControl, string styleClass) { _control = control; KeyControl = keyControl; Button = new Button {StyleClasses = {styleClass}}; UpdateText(); AddChild(Button); Button.OnPressed += args => { control.RebindButtonPressed(this); }; Button.OnKeyBindDown += ButtonOnOnKeyBindDown; CustomMinimumSize = (200, 0); } private void ButtonOnOnKeyBindDown(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.UIRightClick) { if (Binding != null) { _control._deferCommands.Add(() => { _control._inputManager.RemoveBinding(Binding); _control._inputManager.SaveToUserData(); }); } args.Handle(); } } public void UpdateText() { Button.Text = Binding?.GetKeyString() ?? "Unbound"; } } } } }